├── .gitignore ├── .php_cs.dist ├── .scrutinizer.yml ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── ClassUtils.php ├── Exception │ └── GeneratorException.php ├── Generator │ ├── AbstractClassGenerator.php │ ├── AbstractTypeGenerator.php │ └── TypeGenerator.php └── Resources │ └── skeleton │ ├── ArgConfig.php.skeleton │ ├── CustomScalarConfig.php.skeleton │ ├── EnumConfig.php.skeleton │ ├── InputFieldConfig.php.skeleton │ ├── InputObjectConfig.php.skeleton │ ├── InterfaceConfig.php.skeleton │ ├── ObjectConfig.php.skeleton │ ├── OutputFieldConfig.php.skeleton │ ├── TypeSystem.php.skeleton │ ├── UnionConfig.php.skeleton │ └── ValueConfig.php.skeleton └── tests ├── AbstractStarWarsTest.php ├── ClassUtilsTest.php ├── DateTimeType.php ├── Generator ├── AbstractTypeGeneratorTest.php ├── FooInterface.php ├── FooTrait.php ├── TypeGeneratorModeTest.php └── TypeGeneratorTest.php ├── Resolver.php ├── Resources └── Skeleton │ └── TypeSystem.php.skeleton ├── StarWarsData.php ├── StarWarsIntrospectionTest.php ├── StarWarsQueryTest.php ├── TestCase.php └── starWarsSchema.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /bin/ 3 | /composer.lock 4 | /composer.phar 5 | /phpunit.xml 6 | /.php_cs 7 | /.php_cs.cache 8 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | $header = << 16 | 17 | For the full copyright and license information, please view the LICENSE 18 | file that was distributed with this source code. 19 | EOF; 20 | 21 | $finder = PhpCsFixer\Finder::create() 22 | ->exclude('vendor') 23 | ->name('*.php') 24 | ->in([__DIR__.'/src', __DIR__.'/tests']) 25 | ; 26 | 27 | return PhpCsFixer\Config::create() 28 | ->setUsingCache(true) 29 | ->setFinder($finder) 30 | ->setRules([ 31 | '@PSR2' => true, 32 | '@PHP71Migration' => true, 33 | '@PHP71Migration:risky' => true, 34 | 'single_blank_line_before_namespace' => true, 35 | 'ordered_imports' => true, 36 | 'concat_space' => ['spacing' => 'none'], 37 | 'phpdoc_no_alias_tag' => ['type' => 'var'], 38 | 'no_mixed_echo_print' => ['use' => 'echo'], 39 | 'binary_operator_spaces' => ['align_double_arrow' => false, 'align_equals' => false], 40 | 'general_phpdoc_annotation_remove' => ['author', 'category', 'copyright', 'created', 'license', 'package', 'since', 'subpackage', 'version'], 41 | 'native_function_invocation' => true, 42 | 'fully_qualified_strict_types' => true, 43 | 'header_comment' => ['header' => $header], 44 | ]) 45 | ; 46 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | tools: 2 | external_code_coverage: 3 | timeout: 1200 4 | build: 5 | tests: 6 | override: 7 | - true 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | 3 | language: php 4 | 5 | git: 6 | depth: 50 7 | 8 | branches: 9 | only: 10 | - master 11 | - /^\d+\.\d+$/ 12 | 13 | cache: 14 | directories: 15 | - $HOME/.composer/cache 16 | - $HOME/.php_cs.cache 17 | 18 | before_install: 19 | - if [ "${STABILITY}" != "" ]; then perl -pi -e 's/^}$/,"minimum-stability":"'"${STABILITY}"'"}/' composer.json; fi; 20 | - if [ "${PHPUNIT_VERSION}" != "" ]; then composer req "phpunit/phpunit:${PHPUNIT_VERSION}" --dev --no-update; fi; 21 | - mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{,.disabled} || echo "xdebug not available" 22 | - composer selfupdate 23 | - if [ $GRAPHQLPHP_VERSION ]; then composer require "webonyx/graphql-php:${GRAPHQLPHP_VERSION}" --dev --no-update; fi; 24 | 25 | install: travis_retry composer update --prefer-source --no-interaction --optimize-autoloader ${COMPOSER_UPDATE_FLAGS} 26 | 27 | script: bin/phpunit --color=always -v --debug 28 | 29 | jobs: 30 | include: 31 | - stage: Test 32 | - php: 7.1 33 | - php: 7.1 34 | env: COMPOSER_UPDATE_FLAGS=--prefer-lowest 35 | - php: 7.2 36 | - php: 7.3 37 | - php: nightly 38 | env: COMPOSER_UPDATE_FLAGS=--ignore-platform-reqs 39 | 40 | - stage: Code Quality 41 | php: 7.2 42 | env: COVERAGE 43 | before_script: 44 | - mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{.disabled,} 45 | - if [[ ! $(php -m | grep -si xdebug) ]]; then echo "xdebug required for coverage"; exit 1; fi 46 | script: bin/phpunit --color=always -v --debug -d xdebug.max_nesting_level=1000 --coverage-clover=build/logs/clover.xml 47 | after_script: 48 | - wget https://scrutinizer-ci.com/ocular.phar && travis_retry php ocular.phar code-coverage:upload --format=php-clover build/logs/clover.xml 49 | 50 | - stage: Code Quality 51 | php: 7.2 52 | env: CODING_STANDARDS 53 | script: bin/php-cs-fixer fix --dry-run --diff -v --allow-risky=yes --ansi 54 | 55 | allow_failures: 56 | - php: nightly 57 | env: COMPOSER_UPDATE_FLAGS=--ignore-platform-reqs 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Overblog 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | OverblogGraphQLPhpGenerator 2 | =========================== 3 | 4 | 5 | GraphQL PHP types generator... 6 | 7 | [![Code Coverage](https://scrutinizer-ci.com/g/overblog/GraphQLPhpGenerator/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/overblog/GraphQLPhpGenerator/?branch=master) 8 | [![Build Status](https://travis-ci.org/overblog/GraphQLPhpGenerator.svg?branch=master)](https://travis-ci.org/overblog/GraphQLPhpGenerator) 9 | 10 | Installation 11 | ------------ 12 | 13 | ```bash 14 | composer require overblog/graphql-php-generator 15 | ``` 16 | 17 | Usage 18 | ----- 19 | 20 | ```php 21 | [ 30 | 'type' => 'interface', 31 | 'config' => [ 32 | 'description' => new Expression('\'A character\' ~ \' in the Star Wars Trilogy\''), 33 | 'fields' => [ 34 | 'id' => ['type' => 'String!', 'description' => 'The id of the character.'], 35 | 'name' => ['type' => 'String', 'description' => 'The name of the character.'], 36 | 'friends' => ['type' => '[Character]', 'description' => 'The friends of the character.'], 37 | 'appearsIn' => ['type' => '[Episode]', 'description' => 'Which movies they appear in.'], 38 | ], 39 | 'resolveType' => 'Overblog\\GraphQLGenerator\\Tests\\Resolver::resolveType', 40 | ], 41 | ], 42 | /*...*/ 43 | 'Query' => [ 44 | 'type' => 'object', 45 | 'config' => [ 46 | 'description' => 'A humanoid creature in the Star Wars universe or a faction in the Star Wars saga.', 47 | 'fields' => [ 48 | 'hero' => [ 49 | 'type' => 'Character', 50 | 'args' => [ 51 | 'episode' => [ 52 | 'type' => 'Episode', 53 | 'description' => 'If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode.', 54 | ], 55 | ], 56 | 'resolve' => ['Overblog\\GraphQLGenerator\\Tests\\Resolver', 'getHero'], 57 | ], 58 | ], 59 | ], 60 | /*...*/ 61 | ], 62 | ]; 63 | 64 | $typeGenerator = new TypeGenerator('\\My\\Schema\\NP'); 65 | $classesMap = $typeGenerator->generateClasses($configs, __DIR__ . '/cache/types'); 66 | 67 | $loader->addClassMap($classesMap); 68 | 69 | $schema = new Schema(\My\Schema\NP\QueryType::getInstance()); 70 | ``` 71 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "overblog/graphql-php-generator", 3 | "type": "library", 4 | "description": "GraphQL types generator", 5 | "keywords": ["GraphQL", "type", "generator"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Overblog", 10 | "homepage": "http://www.over-blog.com" 11 | } 12 | ], 13 | "config" : { 14 | "sort-packages": true, 15 | "bin-dir": "bin" 16 | }, 17 | "require": { 18 | "php": ">=7.1", 19 | "webonyx/graphql-php": ">=0.13" 20 | }, 21 | "require-dev": { 22 | "friendsofphp/php-cs-fixer": "^2.14", 23 | "phpunit/phpunit": "^5.7.26 || ^6.0 || ^7.2", 24 | "symfony/expression-language": "^3.4", 25 | "symfony/filesystem": "^3.4", 26 | "symfony/process": "^3.4", 27 | "symfony/yaml": "^3.4" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Overblog\\GraphQLGenerator\\": "src/" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Overblog\\GraphQLGenerator\\Tests\\": "tests/" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | ./tests/ 17 | 18 | 19 | 20 | 21 | 22 | ./ 23 | 24 | ./tests 25 | ./src/Overblog/Resources 26 | ./vendor 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/ClassUtils.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Overblog\GraphQLGenerator; 13 | 14 | abstract class ClassUtils 15 | { 16 | /** 17 | * @codeCoverageIgnore 18 | */ 19 | private function __construct() 20 | { 21 | } 22 | 23 | public static function shortenClassName($definition) 24 | { 25 | $shortName = \substr($definition, \strrpos($definition, '\\') + 1); 26 | 27 | return $shortName; 28 | } 29 | 30 | public static function shortenClassFromCode($code, callable $callback = null) 31 | { 32 | if (null === $callback) { 33 | $callback = function ($matches) { 34 | return static::shortenClassName($matches[1]); 35 | }; 36 | } 37 | 38 | $codeParsed = \preg_replace_callback('@((?:\\\\{1,2}\w+|\w+\\\\{1,2})(?:\w+\\\\{0,2})+)@', $callback, $code); 39 | 40 | return $codeParsed; 41 | } 42 | 43 | public static function cleanClasseName($use) 44 | { 45 | return \ltrim($use, '\\'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Exception/GeneratorException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Overblog\GraphQLGenerator\Exception; 15 | 16 | class GeneratorException extends \RuntimeException 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Generator/AbstractClassGenerator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Overblog\GraphQLGenerator\Generator; 13 | 14 | use Overblog\GraphQLGenerator\ClassUtils; 15 | 16 | abstract class AbstractClassGenerator 17 | { 18 | public const MODE_DRY_RUN = 1; 19 | public const MODE_MAPPING_ONLY = 2; 20 | public const MODE_WRITE = 4; 21 | public const MODE_OVERRIDE = 8; 22 | 23 | protected const SKELETON_FILE_PREFIX = '.php.skeleton'; 24 | 25 | /** 26 | * The namespace that contains all classes. 27 | * 28 | * @var string 29 | */ 30 | private $classNamespace; 31 | 32 | private $internalUseStatements = []; 33 | 34 | private $useStatements = []; 35 | 36 | private $traits = []; 37 | 38 | private $implements = []; 39 | 40 | private $skeletonDirs = []; 41 | 42 | /** 43 | * Number of spaces to use for indention in generated code. 44 | */ 45 | private $numSpaces; 46 | 47 | /** 48 | * The actual spaces to use for indention. 49 | * 50 | * @var string 51 | */ 52 | private $spaces; 53 | 54 | private static $templates = []; 55 | 56 | /** 57 | * @param string $classNamespace The namespace to use for the classes. 58 | * @param string[]|string $skeletonDirs 59 | */ 60 | public function __construct($classNamespace = null, $skeletonDirs = []) 61 | { 62 | $this->setClassNamespace($classNamespace); 63 | $this->setSkeletonDirs($skeletonDirs); 64 | $this->setNumSpaces(4); 65 | } 66 | 67 | public function getClassNamespace() 68 | { 69 | return $this->classNamespace; 70 | } 71 | 72 | public function setClassNamespace($classNamespace): self 73 | { 74 | $this->classNamespace = ClassUtils::cleanClasseName($classNamespace); 75 | 76 | return $this; 77 | } 78 | 79 | public function setSkeletonDirs($skeletonDirs): self 80 | { 81 | $this->skeletonDirs = []; 82 | 83 | if (\is_string($skeletonDirs)) { 84 | $this->addSkeletonDir($skeletonDirs); 85 | } else { 86 | if (!\is_array($skeletonDirs) && !$skeletonDirs instanceof \Traversable) { 87 | throw new \InvalidArgumentException( 88 | \sprintf('Skeleton dirs must be array or object implementing \Traversable interface, "%s" given.', \gettype($skeletonDirs)) 89 | ); 90 | } 91 | 92 | foreach ($skeletonDirs as $skeletonDir) { 93 | $this->addSkeletonDir($skeletonDir); 94 | } 95 | } 96 | 97 | return $this; 98 | } 99 | 100 | public function getSkeletonDirs(bool $withDefault = true): array 101 | { 102 | $skeletonDirs = $this->skeletonDirs ; 103 | 104 | if ($withDefault) { 105 | $skeletonDirs[] = __DIR__.'/../Resources/skeleton'; 106 | } 107 | 108 | return $skeletonDirs; 109 | } 110 | 111 | public function addSkeletonDir($skeletonDir): self 112 | { 113 | if (!\is_string($skeletonDir) && !\is_object($skeletonDir) && !\is_callable([$skeletonDir, '__toString'])) { 114 | throw new \InvalidArgumentException( 115 | \sprintf('Skeleton dir must be string or object implementing __toString, "%s" given.', \gettype($skeletonDir)) 116 | ); 117 | } 118 | 119 | $skeletonDir = (string) $skeletonDir; 120 | 121 | if (!\is_dir($skeletonDir)) { 122 | throw new \InvalidArgumentException(\sprintf('Skeleton dir "%s" not found.', $skeletonDir)); 123 | } 124 | $this->skeletonDirs[] = \realpath($skeletonDir); 125 | 126 | return $this; 127 | } 128 | 129 | 130 | /** 131 | * Sets the number of spaces the exported class should have. 132 | * 133 | * @param integer $numSpaces 134 | * 135 | * @return self 136 | */ 137 | public function setNumSpaces(int $numSpaces): self 138 | { 139 | $this->spaces = \str_repeat(' ', $numSpaces); 140 | $this->numSpaces = $numSpaces; 141 | 142 | return $this; 143 | } 144 | 145 | public function addTrait(string $trait): self 146 | { 147 | $cleanTrait = $this->shortenClassName($trait, false); 148 | if (!\in_array($cleanTrait, $this->traits)) { 149 | $this->traits[] = $cleanTrait; 150 | } 151 | 152 | return $this; 153 | } 154 | 155 | public function clearTraits(): self 156 | { 157 | $this->traits = []; 158 | 159 | return $this; 160 | } 161 | 162 | public function addImplement(string $implement): self 163 | { 164 | $cleanImplement = $this->shortenClassName($implement, false); 165 | if (!\in_array($cleanImplement, $this->implements)) { 166 | $this->implements[] = $cleanImplement; 167 | } 168 | 169 | return $this; 170 | } 171 | 172 | public function clearImplements(): self 173 | { 174 | $this->implements = []; 175 | 176 | return $this; 177 | } 178 | 179 | public function addUseStatement(string $useStatement): self 180 | { 181 | $cleanUse = ClassUtils::cleanClasseName($useStatement); 182 | if (!\in_array($cleanUse, $this->useStatements)) { 183 | $this->useStatements[] = $cleanUse; 184 | } 185 | 186 | return $this; 187 | } 188 | 189 | public function clearUseStatements(): self 190 | { 191 | $this->useStatements = []; 192 | 193 | return $this; 194 | } 195 | 196 | public function getSkeletonContent(string $skeleton, bool $withDefault = true) 197 | { 198 | $skeletonDirs = $this->getSkeletonDirs($withDefault); 199 | 200 | foreach ($skeletonDirs as $skeletonDir) { 201 | $path = $skeletonDir.'/'.$skeleton.static::SKELETON_FILE_PREFIX; 202 | 203 | if (!\file_exists($path)) { 204 | continue; 205 | } 206 | 207 | if (!isset(self::$templates[$path])) { 208 | $content = \trim(\file_get_contents($path)); 209 | 210 | self::$templates[$path] = $content; 211 | } 212 | 213 | return self::$templates[$path]; 214 | } 215 | 216 | throw new \InvalidArgumentException( 217 | \sprintf( 218 | 'Skeleton "%s" could not be found in %s.', 219 | $skeleton, 220 | \implode(', ', $skeletonDirs) 221 | ) 222 | ); 223 | } 224 | 225 | protected function addInternalUseStatement(string $use): void 226 | { 227 | $cleanUse = ClassUtils::cleanClasseName($use); 228 | if (!\in_array($cleanUse, $this->internalUseStatements)) { 229 | $this->internalUseStatements[] = $cleanUse; 230 | } 231 | } 232 | 233 | protected function clearInternalUseStatements(): self 234 | { 235 | $this->internalUseStatements = []; 236 | 237 | return $this; 238 | } 239 | 240 | protected function shortenClassName(string $definition, bool $isInternal = true): string 241 | { 242 | $shortName = ClassUtils::shortenClassName($definition); 243 | 244 | $useStatement = \preg_replace('@\:\:.*$@i', '', $definition); 245 | if ($isInternal) { 246 | $this->addInternalUseStatement($useStatement); 247 | } else { 248 | $this->addUseStatement($useStatement); 249 | } 250 | 251 | return $shortName; 252 | } 253 | 254 | protected function shortenClassFromCode(?string $code): string 255 | { 256 | $codeParsed = ClassUtils::shortenClassFromCode( 257 | $code, 258 | function ($matches) { 259 | return $this->shortenClassName($matches[1]); 260 | } 261 | ); 262 | 263 | return $codeParsed; 264 | } 265 | 266 | protected function processPlaceHoldersReplacements(array $placeHolders, string $content, array $values): string 267 | { 268 | $replacements = []; 269 | 270 | foreach ($placeHolders as $placeHolder) { 271 | $generator = [$this, 'generate'.\ucfirst($placeHolder)]; 272 | $name = '<'.$placeHolder.'>'; 273 | 274 | if (\is_callable($generator)) { 275 | $replacements[$name] = \call_user_func_array($generator, [$values]); 276 | } else { 277 | throw new \RuntimeException( 278 | \sprintf( 279 | 'Generator [%s] for placeholder "%s" is not callable.', 280 | \get_class($generator[0]).'::'.$generator[1], 281 | $placeHolder 282 | ) 283 | ); 284 | } 285 | } 286 | 287 | return \strtr($content, $replacements); 288 | } 289 | 290 | protected function processTemplatePlaceHoldersReplacements(string $template, array $values, array $skip = []): string 291 | { 292 | $code = $this->getSkeletonContent($template); 293 | $placeHolders = $this->getPlaceHolders($code); 294 | $code = $this->processPlaceHoldersReplacements(\array_diff($placeHolders, $skip), $code, $values); 295 | 296 | return $code; 297 | } 298 | 299 | protected function getPlaceHolders(string $content): array 300 | { 301 | \preg_match_all('@<([\w]+)>@i', $content, $placeHolders); 302 | 303 | return $placeHolders[1] ?? []; 304 | } 305 | 306 | /** 307 | * @param string $code Code to prefix 308 | * @param int $num Number of indents 309 | * @param bool $skipFirst Skip first line 310 | * 311 | * @return string 312 | */ 313 | protected function prefixCodeWithSpaces(string $code, int $num = 1, bool $skipFirst = true): string 314 | { 315 | $lines = \explode("\n", $code); 316 | 317 | foreach ($lines as $key => $value) { 318 | if (!empty($value)) { 319 | $lines[$key] = \str_repeat($this->spaces, $num).$lines[$key]; 320 | } 321 | } 322 | 323 | if ($skipFirst) { 324 | $lines[0] = \ltrim($lines[0]); 325 | } 326 | 327 | return \implode("\n", $lines); 328 | } 329 | 330 | protected function generateSpaces(): string 331 | { 332 | return $this->spaces; 333 | } 334 | 335 | protected function generateNamespace(): ?string 336 | { 337 | return null !== $this->classNamespace ? 'namespace '.$this->classNamespace.';' : null; 338 | } 339 | 340 | protected function generateUseStatement(array $config): string 341 | { 342 | $statements = \array_merge($this->internalUseStatements, $this->useStatements); 343 | \sort($statements); 344 | 345 | $useStatements = $this->tokenizeUseStatements($statements); 346 | 347 | return $useStatements; 348 | } 349 | 350 | protected function generateClassType(): string 351 | { 352 | return 'final '; 353 | } 354 | 355 | protected function generateImplements(): ?string 356 | { 357 | return \count($this->implements) ? ' implements '.\implode(', ', $this->implements) : null; 358 | } 359 | 360 | protected function generateTraits(): ?string 361 | { 362 | $traits = $this->tokenizeUseStatements($this->traits, ''); 363 | 364 | return $traits ? $traits."\n" : $traits; 365 | } 366 | 367 | protected function tokenizeUseStatements(array $useStatements, $prefix = ''): ?string 368 | { 369 | if (empty($useStatements)) { 370 | return null; 371 | } 372 | 373 | $code = ''; 374 | 375 | foreach ($useStatements as $useStatement) { 376 | $code .= "\n${prefix}use $useStatement;"; 377 | } 378 | 379 | return $code; 380 | } 381 | 382 | /** 383 | * Generates classes files. 384 | * 385 | * @param array $configs raw configs 386 | * @param string $outputDirectory 387 | * @param int $mode 388 | * 389 | * @return array classes map [[FQCLN => classPath], [FQCLN => classPath], ...] 390 | */ 391 | abstract public function generateClasses(array $configs, ?string $outputDirectory, int $mode = self::MODE_WRITE): array; 392 | 393 | /** 394 | * Generates a class file. 395 | * 396 | * @param array $config 397 | * @param string $outputDirectory 398 | * @param int $mode 399 | * 400 | * @return array classes map [FQCLN => classPath] 401 | */ 402 | abstract public function generateClass(array $config, ?string $outputDirectory, int $mode = self::MODE_WRITE): array; 403 | } 404 | -------------------------------------------------------------------------------- /src/Generator/AbstractTypeGenerator.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace Overblog\GraphQLGenerator\Generator; 15 | 16 | use GraphQL\Type\Definition\CustomScalarType; 17 | use GraphQL\Type\Definition\EnumType; 18 | use GraphQL\Type\Definition\InputObjectType; 19 | use GraphQL\Type\Definition\InterfaceType; 20 | use GraphQL\Type\Definition\ObjectType; 21 | use GraphQL\Type\Definition\Type; 22 | use GraphQL\Type\Definition\UnionType; 23 | use Symfony\Component\ExpressionLanguage\Expression; 24 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage; 25 | 26 | abstract class AbstractTypeGenerator extends AbstractClassGenerator 27 | { 28 | public const DEFAULT_CLASS_NAMESPACE = 'Overblog\\CG\\GraphQLGenerator\\__Schema__'; 29 | 30 | protected const DEFERRED_PLACEHOLDERS = ['useStatement', 'spaces', 'closureUseStatements']; 31 | 32 | protected const CLOSURE_TEMPLATE = <<{ 34 | %sreturn %s; 35 | } 36 | EOF; 37 | 38 | private const TYPE_SYSTEMS = [ 39 | 'object' => ObjectType::class, 40 | 'interface' => InterfaceType::class, 41 | 'enum' => EnumType::class, 42 | 'union' => UnionType::class, 43 | 'input-object' => InputObjectType::class, 44 | 'custom-scalar' => CustomScalarType::class, 45 | ]; 46 | 47 | private const INTERNAL_TYPES = [ 48 | Type::STRING => '\\GraphQL\\Type\\Definition\\Type::string()', 49 | Type::INT => '\\GraphQL\\Type\\Definition\\Type::int()', 50 | Type::FLOAT => '\\GraphQL\\Type\\Definition\\Type::float()', 51 | Type::BOOLEAN => '\\GraphQL\\Type\\Definition\\Type::boolean()', 52 | Type::ID => '\\GraphQL\\Type\\Definition\\Type::id()', 53 | ]; 54 | 55 | private const WRAPPED_TYPES = [ 56 | 'NonNull' => '\\GraphQL\\Type\\Definition\\Type::nonNull', 57 | 'ListOf' => '\\GraphQL\\Type\\Definition\\Type::listOf', 58 | ]; 59 | 60 | private $canManageExpressionLanguage = false; 61 | 62 | /** 63 | * @var null|ExpressionLanguage 64 | */ 65 | private $expressionLanguage; 66 | 67 | /** 68 | * @var int 69 | */ 70 | protected $cacheDirMask; 71 | 72 | /** 73 | * @var string 74 | */ 75 | protected $currentlyGeneratedClass; 76 | 77 | /** 78 | * @param string $classNamespace The namespace to use for the classes. 79 | * @param string[]|string $skeletonDirs 80 | * @param int $cacheDirMask 81 | */ 82 | public function __construct(string $classNamespace = self::DEFAULT_CLASS_NAMESPACE, $skeletonDirs = [], int $cacheDirMask = 0775) 83 | { 84 | parent::__construct($classNamespace, $skeletonDirs); 85 | $this->cacheDirMask = $cacheDirMask; 86 | } 87 | 88 | public function setExpressionLanguage(ExpressionLanguage $expressionLanguage = null): self 89 | { 90 | $this->expressionLanguage = $expressionLanguage; 91 | $this->canManageExpressionLanguage = null !== $expressionLanguage; 92 | 93 | return $this; 94 | } 95 | 96 | public function getExpressionLanguage(): ExpressionLanguage 97 | { 98 | return $this->expressionLanguage; 99 | } 100 | 101 | public function isExpression($str): bool 102 | { 103 | return $this->canManageExpressionLanguage && $str instanceof Expression; 104 | } 105 | 106 | public static function getInternalTypes(string $name): ?string 107 | { 108 | return isset(self::INTERNAL_TYPES[$name]) ? self::INTERNAL_TYPES[$name] : null; 109 | } 110 | 111 | public static function getWrappedType(string $name): ?string 112 | { 113 | return isset(self::WRAPPED_TYPES[$name]) ? self::WRAPPED_TYPES[$name] : null; 114 | } 115 | 116 | protected function generateParentClassName(array $config): string 117 | { 118 | return $this->shortenClassName(self::TYPE_SYSTEMS[$config['type']]); 119 | } 120 | 121 | protected function generateClassName(array $config): string 122 | { 123 | return $config['config']['name'].'Type'; 124 | } 125 | 126 | protected function generateClassDocBlock(array $config): string 127 | { 128 | $className = $this->generateClassName($config); 129 | $namespace = $this->getClassNamespace(); 130 | 131 | return <<varExport($values[$key], $default, $compilerNames); 147 | 148 | return $code; 149 | } 150 | 151 | protected function varExport($var, ?string $default = null, array $compilerNames = []): ?string 152 | { 153 | switch (true) { 154 | case \is_array($var): 155 | $indexed = \array_keys($var) === \range(0, \count($var) - 1); 156 | $r = []; 157 | foreach ($var as $key => $value) { 158 | $r[] = ($indexed ? '' : $this->varExport($key, $default).' => ') 159 | .$this->varExport($value, $default); 160 | } 161 | return "[".\implode(", ", $r)."]"; 162 | 163 | case $this->isExpression($var): 164 | return $code = $this->getExpressionLanguage()->compile($var, $compilerNames); 165 | 166 | case \is_object($var): 167 | return $default; 168 | 169 | case \is_string($var): 170 | $string = \var_export($var, true); 171 | 172 | // handle multi-line strings 173 | $lines = \explode("\n", $string); 174 | if (\count($lines) > 1) { 175 | $firstLine = \sprintf('%s\' . "\n"', \array_shift($lines)); 176 | $lastLine = \sprintf("'%s", \array_pop($lines)); 177 | $lines = \array_map( 178 | function ($line) { 179 | return \sprintf('\'%s\' . "\n"', $line); 180 | }, 181 | $lines 182 | ); 183 | \array_unshift($lines, $firstLine); 184 | \array_push($lines, $lastLine); 185 | $string = \implode(' . ', $lines); 186 | } 187 | 188 | return $string; 189 | 190 | default: 191 | return \var_export($var, true); 192 | } 193 | } 194 | 195 | protected function processFromArray(array $values, string $templatePrefix) 196 | { 197 | $code = ''; 198 | 199 | foreach ($values as $name => $value) { 200 | $value['name'] = $value['name'] ?? $name; 201 | $code .= "\n".$this->processTemplatePlaceHoldersReplacements($templatePrefix.'Config', $value); 202 | } 203 | 204 | return '['.$this->prefixCodeWithSpaces($code, 2)."\n]"; 205 | } 206 | 207 | protected function callableCallbackFromArrayValue(array $value, string $key, ?string $argDefinitions = null, string $default = 'null', array $compilerNames = null) 208 | { 209 | if (!$this->arrayKeyExistsAndIsNotNull($value, $key)) { 210 | return $default; 211 | } 212 | 213 | $code = static::CLOSURE_TEMPLATE; 214 | 215 | if (\is_callable($value[$key])) { 216 | $func = $value[$key]; 217 | $code = \sprintf($code, null, null, '\call_user_func_array(%s, \func_get_args())'); 218 | 219 | if (\is_array($func) && isset($func[0]) && \is_string($func[0])) { 220 | $code = \sprintf($code, $this->varExport($func)); 221 | 222 | return $code; 223 | } elseif (\is_string($func)) { 224 | $code = \sprintf($code, \var_export($func, true)); 225 | 226 | return $code; 227 | } 228 | } elseif ($this->isExpression($value[$key])) { 229 | if (null === $compilerNames) { 230 | $compilerNames = []; 231 | if (null !== $argDefinitions) { 232 | \preg_match_all('@\$([a-z_][a-z0-9_]+)@i', $argDefinitions, $matches); 233 | $compilerNames = $matches[1] ?? []; 234 | } 235 | } 236 | 237 | $extraCode = $this->generateExtraCode($value, $key, $argDefinitions, $default, $compilerNames); 238 | 239 | $code = \sprintf( 240 | $code, 241 | $this->shortenClassFromCode($argDefinitions), 242 | $extraCode, 243 | $this->getExpressionLanguage()->compile($value[$key], $compilerNames) 244 | ); 245 | 246 | return $code; 247 | } elseif (!\is_object($value[$key])) { 248 | $code = \sprintf($code, null, null, $this->varExportFromArrayValue($value, $key, $default)); 249 | 250 | return $code; 251 | } 252 | 253 | return $default; 254 | } 255 | 256 | protected function generateConfig(array $config) 257 | { 258 | $template = \str_replace(' ', '', \ucwords(\str_replace('-', ' ', $config['type']))).'Config'; 259 | $code = $this->processTemplatePlaceHoldersReplacements($template, $config['config']); 260 | $code = \ltrim($this->prefixCodeWithSpaces($code, 2)); 261 | 262 | return $code; 263 | } 264 | 265 | protected function generateClosureUseStatements(array $config): ?string 266 | { 267 | return null; 268 | } 269 | 270 | protected function typeAlias2String($alias): string 271 | { 272 | // Non-Null 273 | if ('!' === $alias[\strlen($alias) - 1]) { 274 | return \sprintf('%s(%s)', $this->shortenClassName(static::getWrappedType('NonNull')), $this->typeAlias2String(\substr($alias, 0, -1))); 275 | } 276 | // List 277 | if ('[' === $alias[0]) { 278 | $got = $alias[\strlen($alias) - 1]; 279 | if (']' !== $got) { 280 | throw new \RuntimeException( 281 | \sprintf('Malformed ListOf wrapper type %s expected "]" but got %s.', \json_encode($alias), \json_encode($got)) 282 | ); 283 | } 284 | 285 | return \sprintf('%s(%s)', $this->shortenClassName(static::getWrappedType('ListOf')), $this->typeAlias2String(\substr($alias, 1, -1))); 286 | } 287 | 288 | if (null !== ($systemType = static::getInternalTypes($alias))) { 289 | return $this->shortenClassName($systemType); 290 | } 291 | 292 | return $this->resolveTypeCode($alias); 293 | } 294 | 295 | protected function resolveTypeCode(string $alias): string 296 | { 297 | return $alias.'Type::getInstance()'; 298 | } 299 | 300 | protected function resolveTypesCode(array $values, string $key): string 301 | { 302 | if (isset($values[$key])) { 303 | $types = \sprintf(static::CLOSURE_TEMPLATE, '', '', $this->types2String($values[$key])); 304 | } else { 305 | $types = '[]'; 306 | } 307 | 308 | return $types; 309 | } 310 | 311 | protected function types2String(array $types): string 312 | { 313 | $types = \array_map(__CLASS__.'::typeAlias2String', $types); 314 | 315 | return '['.\implode(', ', $types).']'; 316 | } 317 | 318 | protected function arrayKeyExistsAndIsNotNull(array $value, $key): bool 319 | { 320 | return \array_key_exists($key, $value) && null !== $value[$key]; 321 | } 322 | 323 | /** 324 | * Configs has the following structure: 325 | * 326 | * [ 327 | * [ 328 | * 'type' => 'object', // the file type 329 | * 'config' => [], // the class config 330 | * ], 331 | * [ 332 | * 'type' => 'interface', 333 | * 'config' => [], 334 | * ], 335 | * //... 336 | * ] 337 | * 338 | * 339 | * @param array $configs 340 | * @param string $outputDirectory 341 | * @param int $mode 342 | * 343 | * @return array 344 | */ 345 | public function generateClasses(array $configs, ?string $outputDirectory, int $mode = self::MODE_WRITE): array 346 | { 347 | $classesMap = []; 348 | 349 | foreach ($configs as $name => $config) { 350 | $config['config']['name'] = $config['config']['name'] ?? $name; 351 | $classMap = $this->generateClass($config, $outputDirectory, $mode); 352 | 353 | $classesMap = \array_merge($classesMap, $classMap); 354 | } 355 | 356 | return $classesMap; 357 | } 358 | 359 | /** 360 | * @param array $config 361 | * @param string $outputDirectory 362 | * @param int $mode 363 | * 364 | * @return array 365 | */ 366 | public function generateClass(array $config, ?string $outputDirectory, int $mode = self::MODE_WRITE): array 367 | { 368 | $this->currentlyGeneratedClass = $config['config']['name']; 369 | 370 | $className = $this->generateClassName($config); 371 | $path = $outputDirectory.'/'.$className.'.php'; 372 | 373 | if (!($mode & self::MODE_MAPPING_ONLY)) { 374 | $this->clearInternalUseStatements(); 375 | $code = $this->processTemplatePlaceHoldersReplacements('TypeSystem', $config, static::DEFERRED_PLACEHOLDERS); 376 | $code = $this->processPlaceHoldersReplacements(static::DEFERRED_PLACEHOLDERS, $code, $config)."\n"; 377 | 378 | if ($mode & self::MODE_WRITE) { 379 | $dir = \dirname($path); 380 | if (!\is_dir($dir)) { 381 | \mkdir($dir, $this->cacheDirMask, true); 382 | } 383 | if (($mode & self::MODE_OVERRIDE) || !\file_exists($path)) { 384 | \file_put_contents($path, $code); 385 | } 386 | } 387 | } 388 | 389 | $this->currentlyGeneratedClass = null; 390 | 391 | return [$this->getClassNamespace().'\\'.$className => $path]; 392 | } 393 | 394 | /** 395 | * Adds an extra code into resolver closure before 'return' statement 396 | * 397 | * @param array $value 398 | * @param string $key 399 | * @param string|null $argDefinitions 400 | * @param string $default 401 | * @param array|null $compilerNames 402 | * @return string|null 403 | */ 404 | abstract protected function generateExtraCode(array $value, string $key, ?string $argDefinitions = null, string $default = 'null', array &$compilerNames = null): ?string; 405 | } 406 | -------------------------------------------------------------------------------- /src/Generator/TypeGenerator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Overblog\GraphQLGenerator\Generator; 13 | 14 | class TypeGenerator extends AbstractTypeGenerator 15 | { 16 | protected function generateOutputFields(array $config): string 17 | { 18 | return \sprintf(static::CLOSURE_TEMPLATE, '', '', $this->processFromArray($config['fields'], 'OutputField')); 19 | } 20 | 21 | protected function generateInputFields(array $config): string 22 | { 23 | return \sprintf(static::CLOSURE_TEMPLATE, '', '', $this->processFromArray($config['fields'], 'InputField')); 24 | } 25 | 26 | protected function generateArgs(array $fields): string 27 | { 28 | return isset($fields['args']) ? $this->processFromArray($fields['args'], 'Arg') : '[]'; 29 | } 30 | 31 | protected function generateValues(array $config): string 32 | { 33 | return $this->processFromArray($config['values'], 'Value'); 34 | } 35 | 36 | protected function generateValue(array $value): string 37 | { 38 | return $this->varExportFromArrayValue($value, 'value'); 39 | } 40 | 41 | protected function generateDescription(array $value): string 42 | { 43 | return $this->varExportFromArrayValue($value, 'description'); 44 | } 45 | 46 | protected function generateName(array $value): string 47 | { 48 | return $this->varExportFromArrayValue($value, 'name'); 49 | } 50 | 51 | protected function generateDeprecationReason(array $value): string 52 | { 53 | return $this->varExportFromArrayValue($value, 'deprecationReason'); 54 | } 55 | 56 | protected function generateDefaultValue(array $value): string 57 | { 58 | $key = 'defaultValue'; 59 | if (!\array_key_exists($key, $value)) { 60 | return ''; 61 | } 62 | 63 | return \sprintf("\n'%s' => %s,", $key, $this->varExportFromArrayValue($value, $key)); 64 | } 65 | 66 | protected function generateType(array $value): string 67 | { 68 | $type = 'null'; 69 | 70 | if (isset($value['type'])) { 71 | $type = $this->typeAlias2String($value['type']); 72 | } 73 | 74 | return $type; 75 | } 76 | 77 | protected function generateInterfaces(array $value): string 78 | { 79 | return $this->resolveTypesCode($value, 'interfaces'); 80 | } 81 | 82 | protected function generateTypes(array $value): string 83 | { 84 | return $this->resolveTypesCode($value, 'types'); 85 | } 86 | 87 | protected function generateResolve(array $value): string 88 | { 89 | return $this->callableCallbackFromArrayValue($value, 'resolve', '$value, $args, $context, \\GraphQL\\Type\\Definition\\ResolveInfo $info'); 90 | } 91 | 92 | protected function generateResolveType(array $value): string 93 | { 94 | return $this->callableCallbackFromArrayValue($value, 'resolveType', '$value, $context, \\GraphQL\\Type\\Definition\\ResolveInfo $info'); 95 | } 96 | 97 | protected function generateIsTypeOf(array $value): string 98 | { 99 | return $this->callableCallbackFromArrayValue($value, 'isTypeOf', '$value, $context, \\GraphQL\\Type\\Definition\\ResolveInfo $info'); 100 | } 101 | 102 | protected function generateResolveField(array $value): string 103 | { 104 | return $this->callableCallbackFromArrayValue($value, 'resolveField', '$value, $args, $context, \\GraphQL\\Type\\Definition\\ResolveInfo $info'); 105 | } 106 | 107 | protected function generateComplexity(array $value): string 108 | { 109 | return $this->callableCallbackFromArrayValue($value, 'complexity', '$childrenComplexity, $args = []'); 110 | } 111 | 112 | protected function generateSerialize(array $value): string 113 | { 114 | return $this->callableCallbackFromArrayValue($value, 'serialize', '$value'); 115 | } 116 | 117 | protected function generateParseValue(array $value): string 118 | { 119 | return $this->callableCallbackFromArrayValue($value, 'parseValue', '$value'); 120 | } 121 | 122 | protected function generateParseLiteral(array $value): string 123 | { 124 | return $this->callableCallbackFromArrayValue($value, 'parseLiteral', '$value'); 125 | } 126 | 127 | protected function generateExtraCode(array $value, string $key, ?string $argDefinitions = null, string $default = 'null', array &$compilerNames = null): string 128 | { 129 | return ''; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Resources/skeleton/ArgConfig.php.skeleton: -------------------------------------------------------------------------------- 1 | [ 2 | 'name' => , 3 | 'type' => , 4 | 'description' => , 5 | ], 6 | -------------------------------------------------------------------------------- /src/Resources/skeleton/CustomScalarConfig.php.skeleton: -------------------------------------------------------------------------------- 1 | [ 2 | 'name' => , 3 | 'description' => , 4 | 'serialize' => , 5 | 'parseValue' => , 6 | 'parseLiteral' => , 7 | ] 8 | -------------------------------------------------------------------------------- /src/Resources/skeleton/EnumConfig.php.skeleton: -------------------------------------------------------------------------------- 1 | [ 2 | 'name' => , 3 | 'values' => , 4 | 'description' => , 5 | ] 6 | -------------------------------------------------------------------------------- /src/Resources/skeleton/InputFieldConfig.php.skeleton: -------------------------------------------------------------------------------- 1 | => [ 2 | 'type' => , 3 | 'description' => , 4 | ], 5 | -------------------------------------------------------------------------------- /src/Resources/skeleton/InputObjectConfig.php.skeleton: -------------------------------------------------------------------------------- 1 | [ 2 | 'name' => , 3 | 'description' => , 4 | 'fields' => , 5 | ] 6 | -------------------------------------------------------------------------------- /src/Resources/skeleton/InterfaceConfig.php.skeleton: -------------------------------------------------------------------------------- 1 | [ 2 | 'name' => , 3 | 'description' => , 4 | 'fields' => , 5 | 'resolveType' => , 6 | ] 7 | -------------------------------------------------------------------------------- /src/Resources/skeleton/ObjectConfig.php.skeleton: -------------------------------------------------------------------------------- 1 | [ 2 | 'name' => , 3 | 'description' => , 4 | 'fields' => , 5 | 'interfaces' => , 6 | 'isTypeOf' => , 7 | 'resolveField' => , 8 | ] 9 | -------------------------------------------------------------------------------- /src/Resources/skeleton/OutputFieldConfig.php.skeleton: -------------------------------------------------------------------------------- 1 | => [ 2 | 'type' => , 3 | 'args' => , 4 | 'resolve' => , 5 | 'description' => , 6 | 'deprecationReason' => , 7 | 'complexity' => , 8 | ], 9 | -------------------------------------------------------------------------------- /src/Resources/skeleton/TypeSystem.php.skeleton: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | class extends 6 | { 7 | /** 8 | * @var static 9 | */ 10 | private static $instance; 11 | 12 | /** 13 | * @return static 14 | */ 15 | public static function getInstance() 16 | { 17 | return null !== self::$instance ? self::$instance : self::$instance = new static(); 18 | } 19 | 20 | public static function clearInstance() 21 | { 22 | self::$instance = null; 23 | } 24 | 25 | public function __construct() 26 | { 27 | if (isset(static::$instance)) { 28 | throw new \DomainException('You can not create more than one copy of a singleton.'); 29 | } 30 | parent::__construct(); 31 | static::$instance = $this; 32 | } 33 | 34 | public function __clone() 35 | { 36 | throw new \DomainException('You can not clone a singleton.'); 37 | } 38 | 39 | public function __destruct() 40 | { 41 | $this->clearInstance(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Resources/skeleton/UnionConfig.php.skeleton: -------------------------------------------------------------------------------- 1 | [ 2 | 'name' => , 3 | 'types' => , 4 | 'resolveType' => , 5 | 'description' => , 6 | ] 7 | -------------------------------------------------------------------------------- /src/Resources/skeleton/ValueConfig.php.skeleton: -------------------------------------------------------------------------------- 1 | => [ 2 | 'name' => , 3 | 'value' => , 4 | 'deprecationReason' => , 5 | 'description' => , 6 | ], 7 | -------------------------------------------------------------------------------- /tests/AbstractStarWarsTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Overblog\GraphQLGenerator\Tests; 13 | 14 | use GraphQL\Error\Debug; 15 | use GraphQL\Error\DebugFlag; 16 | use GraphQL\GraphQL; 17 | use GraphQL\Type\Schema; 18 | use Overblog\GraphQLGenerator\Tests\Generator\AbstractTypeGeneratorTest; 19 | 20 | abstract class AbstractStarWarsTest extends AbstractTypeGeneratorTest 21 | { 22 | /** 23 | * @var Schema 24 | */ 25 | protected $schema; 26 | 27 | public function setUp(): void 28 | { 29 | parent::setUp(); 30 | 31 | $this->generateClasses(); 32 | 33 | Resolver::setHumanType($this->getType('Human')); 34 | Resolver::setDroidType($this->getType('Droid')); 35 | 36 | $this->schema = new Schema(['query' => $this->getType('Query')]); 37 | $this->schema->assertValid(); 38 | } 39 | 40 | protected function assertValidQuery(string $query, array $expected, array $variables = null): void 41 | { 42 | // TODO(mcg-web): remove this when migrating to webonyx/graphql-php >= 14.0 43 | $debug = \class_exists('GraphQL\Error\DebugFlag') ? DebugFlag::class : Debug::class; 44 | 45 | $actual = GraphQL::executeQuery($this->schema, $query, null, null, $variables) 46 | ->toArray($debug::INCLUDE_DEBUG_MESSAGE | $debug::INCLUDE_TRACE); 47 | $expected = ['data' => $expected]; 48 | $this->assertEquals($expected, $actual, \json_encode($actual)); 49 | } 50 | 51 | protected function sortSchemaEntry(array &$entries, string $entryKey, string $sortBy): void 52 | { 53 | if (isset($entries['data']['__schema'][$entryKey])) { 54 | $data = &$entries['data']['__schema'][$entryKey]; 55 | } else { 56 | $data = &$entries['__schema'][$entryKey]; 57 | } 58 | 59 | \usort($data, function ($data1, $data2) use ($sortBy) { 60 | return \strcmp($data1[$sortBy], $data2[$sortBy]); 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/ClassUtilsTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Overblog\GraphQLGenerator\Tests; 13 | 14 | use Overblog\GraphQLGenerator\ClassUtils; 15 | 16 | class ClassUtilsTest extends TestCase 17 | { 18 | /** 19 | * @param $code 20 | * @param $expected 21 | * 22 | * @dataProvider shortenClassFromCodeDataProvider 23 | */ 24 | public function testShortenClassFromCode($code, $expected): void 25 | { 26 | $actual = ClassUtils::shortenClassFromCode($code); 27 | 28 | $this->assertEquals($expected, $actual); 29 | } 30 | 31 | public function shortenClassFromCodeDataProvider(): iterable 32 | { 33 | return [ 34 | ['$toto, \Toto\Tata $test', '$toto, Tata $test'], 35 | ['\Tata $test', 'Tata $test'], 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/DateTimeType.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Overblog\GraphQLGenerator\Tests; 13 | 14 | use GraphQL\Language\AST\Node; 15 | 16 | class DateTimeType 17 | { 18 | /** 19 | * @param \DateTime $value 20 | * 21 | * @return string 22 | */ 23 | public static function serialize(\DateTime $value) 24 | { 25 | return $value->format('Y-m-d H:i:s'); 26 | } 27 | 28 | /** 29 | * @param mixed $value 30 | * 31 | * @return mixed 32 | */ 33 | public static function parseValue($value) 34 | { 35 | return new \DateTime($value); 36 | } 37 | 38 | /** 39 | * @param Node $valueNode 40 | * 41 | * @return string 42 | */ 43 | public static function parseLiteral($valueNode) 44 | { 45 | return new \DateTime($valueNode->value); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Generator/AbstractTypeGeneratorTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Overblog\GraphQLGenerator\Tests\Generator; 13 | 14 | use Composer\Autoload\ClassLoader; 15 | use GraphQL\Type\Definition\Type; 16 | use Overblog\GraphQLGenerator\Generator\TypeGenerator; 17 | use Overblog\GraphQLGenerator\Tests\TestCase; 18 | use Symfony\Component\ExpressionLanguage\Expression; 19 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage; 20 | use Symfony\Component\Filesystem\Filesystem; 21 | 22 | abstract class AbstractTypeGeneratorTest extends TestCase 23 | { 24 | /** @var Filesystem */ 25 | protected $filesystem; 26 | protected $tmpDir; 27 | protected $typeConfigs = []; 28 | /** @var TypeGenerator */ 29 | protected $typeGenerator; 30 | /** @var ClassLoader */ 31 | protected $classLoader; 32 | 33 | public function setUp(): void 34 | { 35 | $this->filesystem = new Filesystem(); 36 | $this->tmpDir = \sys_get_temp_dir().'/overblog-graphql-generator'; 37 | $this->filesystem->remove($this->tmpDir); 38 | $this->typeConfigs = $this->prepareTypeConfigs(); 39 | $this->typeGenerator = new TypeGenerator(); 40 | $this->typeGenerator->setExpressionLanguage(new ExpressionLanguage()); 41 | $this->classLoader = new ClassLoader(); 42 | } 43 | 44 | public function tearDown(): void 45 | { 46 | $this->filesystem->remove($this->tmpDir); 47 | } 48 | 49 | protected function generateClasses(array $typeConfigs = null, ?string $tmpDir = null, int $mode = TypeGenerator::MODE_WRITE): array 50 | { 51 | if (null === $typeConfigs) { 52 | $typeConfigs = $this->typeConfigs; 53 | } 54 | 55 | if (null === $tmpDir) { 56 | $tmpDir = $this->tmpDir; 57 | } 58 | 59 | $classes = $this->typeGenerator->generateClasses($typeConfigs, $tmpDir, $mode); 60 | 61 | $this->classLoader->addClassMap($classes); 62 | $this->classLoader->register(); 63 | 64 | return $classes; 65 | } 66 | 67 | /** 68 | * @return array 69 | */ 70 | protected function prepareTypeConfigs(): array 71 | { 72 | $yaml = new \Symfony\Component\Yaml\Parser(); 73 | $typeConfigs = $yaml->parse(\file_get_contents(__DIR__.'/../starWarsSchema.yml')); 74 | 75 | return $this->processConfig($typeConfigs); 76 | } 77 | 78 | protected function processConfig(array $configs): array 79 | { 80 | return \array_map( 81 | function ($v) { 82 | if (\is_array($v)) { 83 | return \call_user_func([$this, 'processConfig'], $v); 84 | } elseif (\is_string($v) && 0 === \strpos($v, '@=')) { 85 | return new Expression(\substr($v, 2)); 86 | } 87 | 88 | return $v; 89 | }, 90 | $configs 91 | ); 92 | } 93 | 94 | protected function getType($type): Type 95 | { 96 | return \call_user_func(["\\".$this->typeGenerator->getClassNamespace().'\\'.$type.'Type', 'getInstance']); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/Generator/FooInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Overblog\GraphQLGenerator\Tests\Generator; 13 | 14 | interface FooInterface 15 | { 16 | public function bar(): string; 17 | } 18 | -------------------------------------------------------------------------------- /tests/Generator/FooTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Overblog\GraphQLGenerator\Tests\Generator; 13 | 14 | trait FooTrait 15 | { 16 | public function bar(): string 17 | { 18 | return 'Foo::bar'; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Generator/TypeGeneratorModeTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Overblog\GraphQLGenerator\Tests\Generator; 13 | 14 | use Overblog\GraphQLGenerator\Generator\TypeGenerator; 15 | use Overblog\GraphQLGenerator\Tests\TestCase; 16 | use PHPUnit\Framework\MockObject\MockObject; 17 | 18 | class TypeGeneratorModeTest extends TestCase 19 | { 20 | /** @var string */ 21 | private $dir; 22 | 23 | /** @var TypeGenerator|MockObject */ 24 | private $typeGenerator; 25 | 26 | private const CONFIG = [ 27 | 'Query' => [ 28 | 'type' => 'object', 29 | 'config' => [ 30 | 'fields' => [ 31 | 'sayHello' => ['type' => 'String!'], 32 | ], 33 | ], 34 | ] 35 | ]; 36 | 37 | public function setUp(): void 38 | { 39 | $this->dir = \sys_get_temp_dir().'/overblog-graphql-generator-modes'; 40 | $this->typeGenerator = $this->getMockBuilder(TypeGenerator::class) 41 | ->setMethods(['processPlaceHoldersReplacements', 'processTemplatePlaceHoldersReplacements']) 42 | ->getMock() 43 | ; 44 | } 45 | 46 | public function testDryRunMode(): void 47 | { 48 | $this->typeGenerator->expects($this->once())->method('processPlaceHoldersReplacements'); 49 | $this->typeGenerator->expects($this->once())->method('processTemplatePlaceHoldersReplacements'); 50 | $this->assertGenerateClassesMode(TypeGenerator::MODE_DRY_RUN); 51 | } 52 | 53 | public function testMappingOnlyMode(): void 54 | { 55 | $this->typeGenerator->expects($this->never())->method('processPlaceHoldersReplacements'); 56 | $this->typeGenerator->expects($this->never())->method('processTemplatePlaceHoldersReplacements'); 57 | $this->assertGenerateClassesMode(TypeGenerator::MODE_MAPPING_ONLY); 58 | } 59 | 60 | private function assertGenerateClassesMode($mode): void 61 | { 62 | $classes = $this->typeGenerator->generateClasses(self::CONFIG, $this->dir, $mode); 63 | $file = $this->dir.'/QueryType.php'; 64 | $this->assertEquals(['Overblog\CG\GraphQLGenerator\__Schema__\QueryType' => $this->dir.'/QueryType.php'], $classes); 65 | if (\method_exists($this, 'assertDirectoryNotExists')) { 66 | $this->assertDirectoryNotExists($this->dir); 67 | } else { // for phpunit 4 68 | $this->assertFalse(\file_exists($this->dir)); 69 | $this->assertFalse(\is_dir($this->dir)); 70 | } 71 | if (\method_exists($this, 'assertFileNotExists')) { 72 | $this->assertFileNotExists($file); 73 | } else { // for phpunit 4 74 | $this->assertFalse(\file_exists($file)); 75 | $this->assertFalse(\is_file($file)); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Generator/TypeGeneratorTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Overblog\GraphQLGenerator\Tests\Generator; 13 | 14 | use GraphQL\Type\Definition\BooleanType; 15 | use GraphQL\Type\Definition\FloatType; 16 | use GraphQL\Type\Definition\IDType; 17 | use GraphQL\Type\Definition\IntType; 18 | use GraphQL\Type\Definition\ObjectType; 19 | use GraphQL\Type\Definition\StringType; 20 | use GraphQL\Type\Definition\Type; 21 | use Overblog\GraphQLGenerator\Generator\TypeGenerator; 22 | 23 | class TypeGeneratorTest extends AbstractTypeGeneratorTest 24 | { 25 | public function testWrongSetSkeletonDirs(): void 26 | { 27 | $this->expectException(\InvalidArgumentException::class); 28 | $this->expectExceptionMessage('Skeleton dir "fake" not found.'); 29 | $this->typeGenerator->setSkeletonDirs(['fake']); 30 | } 31 | 32 | public function testWrongAddSkeletonDir(): void 33 | { 34 | $this->expectException(\InvalidArgumentException::class); 35 | $this->expectExceptionMessage('Skeleton dir must be string or object implementing __toString, "array" given.'); 36 | $this->typeGenerator->addSkeletonDir([]); 37 | } 38 | 39 | public function testWrongObjectSetSkeletonDir(): void 40 | { 41 | $this->expectException(\InvalidArgumentException::class); 42 | $this->expectExceptionMessage('Skeleton dirs must be array or object implementing \Traversable interface, "object" given.'); 43 | $this->typeGenerator->setSkeletonDirs(new \stdClass()); 44 | } 45 | 46 | public function testWrongGetSkeletonDirs(): void 47 | { 48 | $this->expectException(\InvalidArgumentException::class); 49 | $this->expectExceptionMessageRegExp('/Skeleton "fake" could not be found in .*\/skeleton./'); 50 | $this->typeGenerator->getSkeletonContent('fake'); 51 | } 52 | 53 | public function testTypeAlias2String(): void 54 | { 55 | $this->generateClasses($this->getConfigs()); 56 | 57 | /** @var ObjectType $type */ 58 | $type = $this->getType('T'); 59 | 60 | $this->assertInstanceOf(StringType::class, $type->getField('string')->getType()); 61 | $this->assertInstanceOf(IntType::class, $type->getField('int')->getType()); 62 | $this->assertInstanceOf(IDType::class, $type->getField('id')->getType()); 63 | $this->assertInstanceOf(FloatType::class, $type->getField('float')->getType()); 64 | $this->assertInstanceOf(BooleanType::class, $type->getField('boolean')->getType()); 65 | 66 | $this->assertEquals(Type::nonNull(Type::string()), $type->getField('nonNullString')->getType()); 67 | $this->assertEquals(Type::listOf(Type::string()), $type->getField('listOfString')->getType()); 68 | $this->assertEquals(Type::listOf(Type::listOf(Type::string())), $type->getField('listOfListOfString')->getType()); 69 | $this->assertEquals( 70 | Type::nonNull( 71 | Type::listOf( 72 | Type::nonNull( 73 | Type::listOf( 74 | Type::nonNull(Type::string()) 75 | ) 76 | ) 77 | ) 78 | ), 79 | $type->getField('listOfListOfStringNonNull')->getType() 80 | ); 81 | } 82 | 83 | public function testTypeAlias2StringInvalidListOf(): void 84 | { 85 | $this->expectException(\RuntimeException::class); 86 | $this->expectExceptionMessage('Malformed ListOf wrapper type "[String" expected "]" but got "g".'); 87 | $this->generateClasses([ 88 | 'T' => [ 89 | 'type' => 'object', 90 | 'config' => [ 91 | 'fields' => [ 92 | 'invalidlistOfString' => ['type' => '[String'], 93 | ] 94 | ], 95 | ] 96 | ]); 97 | } 98 | 99 | public function testAddTraitAndClearTraits(): void 100 | { 101 | $trait = FooTrait::class; 102 | $interface = FooInterface::class; 103 | $this->typeGenerator->addTrait($trait) 104 | ->addImplement($interface); 105 | $this->generateClasses(['U' => $this->getConfigs()['T']]); 106 | 107 | /** @var FooInterface|ObjectType $type */ 108 | $type = $this->getType('U'); 109 | 110 | $this->assertInstanceOf($interface, $type); 111 | $this->assertEquals('Foo::bar', $type->bar()); 112 | 113 | $this->typeGenerator->clearTraits() 114 | ->clearImplements() 115 | ->clearUseStatements(); 116 | $this->generateClasses(['V' => $this->getConfigs()['T']]); 117 | 118 | /** @var ObjectType $type */ 119 | $type = $this->getType('V'); 120 | 121 | $this->assertNotInstanceOf($interface, $type); 122 | $this->assertFalse(\method_exists($type, 'bar')); 123 | } 124 | 125 | public function testCallbackEntryDoesNotTreatObject(): void 126 | { 127 | $this->generateClasses([ 128 | 'W' => [ 129 | 'type' => 'object', 130 | 131 | 'config' => [ 132 | 'description' => new \stdClass(), 133 | 'fields' => [ 134 | 'resolveObject' => ['type' => '[String]', 'resolve' => new \stdClass()], 135 | 'resolveAnyNotObject' => ['type' => '[String]', 'resolve' => ['result' => 1]], 136 | ] 137 | ], 138 | ] 139 | ]); 140 | 141 | /** @var ObjectType $type */ 142 | $type = $this->getType('W'); 143 | 144 | $this->assertNull($type->getField('resolveObject')->resolveFn); 145 | $this->assertNull($type->getField('resolveObject')->description); 146 | $resolveFn = $type->getField('resolveAnyNotObject')->resolveFn; 147 | $this->assertInstanceOf(\Closure::class, $resolveFn); 148 | $this->assertEquals(['result' => 1], $resolveFn()); 149 | } 150 | 151 | public function testProcessInvalidPlaceHoldersReplacements(): void 152 | { 153 | $this->expectException(\RuntimeException::class); 154 | $this->expectExceptionMessage(\sprintf( 155 | 'Generator [%s::generateFake] for placeholder "fake" is not callable.', 156 | TypeGenerator::class 157 | )); 158 | $this->typeGenerator->setSkeletonDirs(__DIR__.'/../Resources/Skeleton'); 159 | 160 | $this->generateClasses($this->getConfigs()); 161 | } 162 | 163 | public function testTypeSingletonCantBeClone(): void 164 | { 165 | $this->generateClasses($this->getConfigs()); 166 | 167 | /** @var ObjectType $type */ 168 | $type = $this->getType('T'); 169 | 170 | $this->expectException(\DomainException::class); 171 | $this->expectExceptionMessage('You can not clone a singleton.'); 172 | 173 | $t = clone $type; 174 | } 175 | 176 | public function testTypeSingletonCanBeInstantiatedOnlyOnce(): void 177 | { 178 | $this->generateClasses($this->getConfigs()); 179 | 180 | /** @var ObjectType $type */ 181 | $type = $this->getType('T'); 182 | 183 | $this->expectException(\DomainException::class); 184 | $this->expectExceptionMessage('You can not create more than one copy of a singleton.'); 185 | 186 | $class = \get_class($type); 187 | $t = new $class(); 188 | } 189 | 190 | private function getConfigs(): array 191 | { 192 | return [ 193 | 'T' => [ 194 | 'type' => 'object', 195 | 'config' => [ 196 | 'fields' => [ 197 | 'string' => ['type' => 'String'], 198 | 'int' => ['type' => 'Int'], 199 | 'id' => ['type' => 'ID'], 200 | 'float' => ['type' => 'Float'], 201 | 'boolean' => ['type' => 'Boolean'], 202 | 'nonNullString' => ['type' => 'String!'], 203 | 'listOfString' => ['type' => '[String]'], 204 | 'listOfListOfString' => ['type' => '[[String]]'], 205 | 'listOfListOfStringNonNull' => ['type' => '[[String!]!]!'], 206 | ] 207 | ], 208 | ] 209 | ]; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /tests/Resolver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Overblog\GraphQLGenerator\Tests; 13 | 14 | use GraphQL\Type\Definition\Type; 15 | 16 | abstract class Resolver 17 | { 18 | /** @var Type */ 19 | private static $humanType; 20 | 21 | /** @var Type */ 22 | private static $droidType; 23 | 24 | private function __construct() 25 | { 26 | } 27 | 28 | public static function getHumanType(): Type 29 | { 30 | return self::$humanType; 31 | } 32 | 33 | public static function getDroidType(): Type 34 | { 35 | return self::$droidType; 36 | } 37 | 38 | /** 39 | * @param Type $humanType 40 | */ 41 | public static function setHumanType($humanType): void 42 | { 43 | self::$humanType = $humanType; 44 | } 45 | 46 | /** 47 | * @param Type $droidType 48 | */ 49 | public static function setDroidType($droidType): void 50 | { 51 | self::$droidType = $droidType; 52 | } 53 | 54 | public static function resolveType($obj): ?Type 55 | { 56 | $humans = StarWarsData::humans(); 57 | $droids = StarWarsData::droids(); 58 | if (isset($humans[$obj['id']])) { 59 | return static::getHumanType(); 60 | } 61 | if (isset($droids[$obj['id']])) { 62 | return static::getDroidType(); 63 | } 64 | return null; 65 | } 66 | 67 | public static function getFriends($droidOrHuman): array 68 | { 69 | return StarWarsData::getFriends($droidOrHuman); 70 | } 71 | 72 | public static function getHero($root, $args): array 73 | { 74 | return StarWarsData::getHero($args['episode']['name'] ?? null); 75 | } 76 | 77 | public static function getHuman($root, $args): ?array 78 | { 79 | $humans = StarWarsData::humans(); 80 | 81 | return $humans[$args['id']] ?? null; 82 | } 83 | 84 | public static function getDroid($root, $args): array 85 | { 86 | $droids = StarWarsData::droids(); 87 | 88 | return $droids[$args['id']] ?? null; 89 | } 90 | 91 | public static function getDateTime($root, $args): ?\DateTime 92 | { 93 | return $args['dateTime'] ?? new \DateTime('2016-11-28 12:00:00'); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/Resources/Skeleton/TypeSystem.php.skeleton: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/StarWarsData.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Overblog\GraphQLGenerator\Tests; 13 | 14 | /** 15 | * Class StarWarsData 16 | * 17 | * This is a copy of original test file GraphQL\Tests\StarWarsData 18 | * 19 | */ 20 | class StarWarsData 21 | { 22 | private static function luke(): array 23 | { 24 | return [ 25 | 'id' => '1000', 26 | 'name' => 'Luke Skywalker', 27 | 'friends' => ['1002', '1003', '2000', '2001'], 28 | 'appearsIn' => [4, 5, 6], 29 | 'homePlanet' => 'Tatooine', 30 | ]; 31 | } 32 | 33 | private static function vader(): array 34 | { 35 | return [ 36 | 'id' => '1001', 37 | 'name' => 'Darth Vader', 38 | 'friends' => ['1004'], 39 | 'appearsIn' => [4, 5, 6], 40 | 'homePlanet' => 'Tatooine', 41 | ]; 42 | } 43 | 44 | private static function han(): array 45 | { 46 | return [ 47 | 'id' => '1002', 48 | 'name' => 'Han Solo', 49 | 'friends' => ['1000', '1003', '2001'], 50 | 'appearsIn' => [4, 5, 6], 51 | ]; 52 | } 53 | 54 | private static function leia(): array 55 | { 56 | return [ 57 | 'id' => '1003', 58 | 'name' => 'Leia Organa', 59 | 'friends' => ['1000', '1002', '2000', '2001'], 60 | 'appearsIn' => [4, 5, 6], 61 | 'homePlanet' => 'Alderaan', 62 | ]; 63 | } 64 | 65 | private static function tarkin(): array 66 | { 67 | return [ 68 | 'id' => '1004', 69 | 'name' => 'Wilhuff Tarkin', 70 | 'friends' => ['1001'], 71 | 'appearsIn' => [4], 72 | ]; 73 | } 74 | 75 | public static function humans(): array 76 | { 77 | return [ 78 | '1000' => self::luke(), 79 | '1001' => self::vader(), 80 | '1002' => self::han(), 81 | '1003' => self::leia(), 82 | '1004' => self::tarkin(), 83 | ]; 84 | } 85 | 86 | private static function threepio(): array 87 | { 88 | return [ 89 | 'id' => '2000', 90 | 'name' => 'C-3PO', 91 | 'friends' => ['1000', '1002', '1003', '2001'], 92 | 'appearsIn' => [4, 5, 6], 93 | 'primaryFunction' => 'Protocol', 94 | ]; 95 | } 96 | 97 | /** 98 | * We export artoo directly because the schema returns him 99 | * from a root field, and hence needs to reference him. 100 | */ 101 | public static function artoo(): array 102 | { 103 | return [ 104 | 105 | 'id' => '2001', 106 | 'name' => 'R2-D2', 107 | 'friends' => ['1000', '1002', '1003'], 108 | 'appearsIn' => [4, 5, 6], 109 | 'primaryFunction' => 'Astromech', 110 | ]; 111 | } 112 | 113 | public static function droids(): array 114 | { 115 | return [ 116 | '2000' => self::threepio(), 117 | '2001' => self::artoo(), 118 | ]; 119 | } 120 | 121 | /** 122 | * Helper function to get a character by ID. 123 | */ 124 | public static function getCharacter(string $id): array 125 | { 126 | $humans = self::humans(); 127 | $droids = self::droids(); 128 | if (isset($humans[$id])) { 129 | return $humans[$id]; 130 | } 131 | if (isset($droids[$id])) { 132 | return $droids[$id]; 133 | } 134 | return null; 135 | } 136 | 137 | /** 138 | * Allows us to query for a character's friends. 139 | */ 140 | public static function getFriends(array $character): array 141 | { 142 | return \array_map([__CLASS__, 'getCharacter'], $character['friends']); 143 | } 144 | 145 | public static function getHero(?int $episode): array 146 | { 147 | if ($episode === 5) { 148 | // Luke is the hero of Episode V. 149 | return self::luke(); 150 | } 151 | // Artoo is the hero otherwise. 152 | return self::artoo(); 153 | } 154 | 155 | public static function getHuman(string $id): ?array 156 | { 157 | $humans = self::humans(); 158 | 159 | return $humans[$id] ?? null; 160 | } 161 | 162 | public static function getDroid($id): ?array 163 | { 164 | $droids = self::droids(); 165 | 166 | return $droids[$id] ?? null; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /tests/StarWarsIntrospectionTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Overblog\GraphQLGenerator\Tests; 13 | 14 | use GraphQL\GraphQL; 15 | 16 | class StarWarsIntrospectionTest extends AbstractStarWarsTest 17 | { 18 | // Star Wars Introspection Tests 19 | // Basic Introspection 20 | // it('Allows querying the schema for types') 21 | public function testAllowsQueryingTheSchemaForTypes(): void 22 | { 23 | $query = ' 24 | query IntrospectionTypeQuery { 25 | __schema { 26 | types { 27 | name 28 | } 29 | } 30 | } 31 | '; 32 | $expected = [ 33 | '__schema' => [ 34 | 'types' => [ 35 | ['name' => 'ID'], 36 | ['name' => 'String'], 37 | ['name' => 'Float'], 38 | ['name' => 'Int'], 39 | ['name' => 'Boolean'], 40 | ['name' => '__Schema'], 41 | ['name' => '__Type'], 42 | ['name' => '__TypeKind'], 43 | ['name' => '__Field'], 44 | ['name' => '__InputValue'], 45 | ['name' => '__EnumValue'], 46 | ['name' => '__Directive'], 47 | ['name' => '__DirectiveLocation'], 48 | ['name' => 'Query'], 49 | ['name' => 'HeroInput'], 50 | ['name' => 'Episode'], 51 | ['name' => 'Character'], 52 | ['name' => 'Human'], 53 | ['name' => 'Droid'], 54 | ['name' => 'DateTime'], 55 | ] 56 | ] 57 | ]; 58 | 59 | $actual = GraphQL::executeQuery($this->schema, $query)->toArray(); 60 | $this->sortSchemaEntry($actual, 'types', 'name'); 61 | $this->sortSchemaEntry($expected, 'types', 'name'); 62 | $expected = ['data' => $expected]; 63 | $this->assertEquals($expected, $actual, \json_encode($actual)); 64 | } 65 | 66 | // it('Allows querying the schema for query type') 67 | public function testAllowsQueryingTheSchemaForQueryType(): void 68 | { 69 | $query = ' 70 | query IntrospectionQueryTypeQuery { 71 | __schema { 72 | queryType { 73 | name 74 | } 75 | } 76 | } 77 | '; 78 | $expected = [ 79 | '__schema' => [ 80 | 'queryType' => [ 81 | 'name' => 'Query' 82 | ], 83 | ] 84 | ]; 85 | $this->assertValidQuery($query, $expected); 86 | } 87 | 88 | // it('Allows querying the schema for a specific type') 89 | public function testAllowsQueryingTheSchemaForASpecificType(): void 90 | { 91 | $query = ' 92 | query IntrospectionDroidTypeQuery { 93 | __type(name: "Droid") { 94 | name 95 | } 96 | } 97 | '; 98 | $expected = [ 99 | '__type' => [ 100 | 'name' => 'Droid' 101 | ] 102 | ]; 103 | $this->assertValidQuery($query, $expected); 104 | } 105 | 106 | // it('Allows querying the schema for an object kind') 107 | public function testAllowsQueryingForAnObjectKind(): void 108 | { 109 | $query = ' 110 | query IntrospectionDroidKindQuery { 111 | __type(name: "Droid") { 112 | name 113 | kind 114 | } 115 | } 116 | '; 117 | $expected = [ 118 | '__type' => [ 119 | 'name' => 'Droid', 120 | 'kind' => 'OBJECT' 121 | ] 122 | ]; 123 | $this->assertValidQuery($query, $expected); 124 | } 125 | 126 | // it('Allows querying the schema for an interface kind') 127 | public function testAllowsQueryingForInterfaceKind(): void 128 | { 129 | $query = ' 130 | query IntrospectionCharacterKindQuery { 131 | __type(name: "Character") { 132 | name 133 | kind 134 | } 135 | } 136 | '; 137 | $expected = [ 138 | '__type' => [ 139 | 'name' => 'Character', 140 | 'kind' => 'INTERFACE' 141 | ] 142 | ]; 143 | $this->assertValidQuery($query, $expected); 144 | } 145 | 146 | // it('Allows querying the schema for object fields') 147 | public function testAllowsQueryingForObjectFields(): void 148 | { 149 | $query = ' 150 | query IntrospectionDroidFieldsQuery { 151 | __type(name: "Droid") { 152 | name 153 | fields { 154 | name 155 | type { 156 | name 157 | kind 158 | } 159 | } 160 | } 161 | } 162 | '; 163 | $expected = [ 164 | '__type' => [ 165 | 'name' => 'Droid', 166 | 'fields' => [ 167 | [ 168 | 'name' => 'id', 169 | 'type' => [ 170 | 'name' => null, 171 | 'kind' => 'NON_NULL' 172 | ] 173 | ], 174 | [ 175 | 'name' => 'name', 176 | 'type' => [ 177 | 'name' => 'String', 178 | 'kind' => 'SCALAR' 179 | ] 180 | ], 181 | [ 182 | 'name' => 'friends', 183 | 'type' => [ 184 | 'name' => null, 185 | 'kind' => 'LIST' 186 | ] 187 | ], 188 | [ 189 | 'name' => 'appearsIn', 190 | 'type' => [ 191 | 'name' => null, 192 | 'kind' => 'LIST' 193 | ] 194 | ], 195 | [ 196 | 'name' => 'primaryFunction', 197 | 'type' => [ 198 | 'name' => 'String', 199 | 'kind' => 'SCALAR' 200 | ] 201 | ] 202 | ] 203 | ] 204 | ]; 205 | $this->assertValidQuery($query, $expected); 206 | } 207 | 208 | // it('Allows querying the schema for nested object fields') 209 | public function testAllowsQueryingTheSchemaForNestedObjectFields(): void 210 | { 211 | $query = ' 212 | query IntrospectionDroidNestedFieldsQuery { 213 | __type(name: "Droid") { 214 | name 215 | fields { 216 | name 217 | type { 218 | name 219 | kind 220 | ofType { 221 | name 222 | kind 223 | } 224 | } 225 | } 226 | } 227 | } 228 | '; 229 | $expected = [ 230 | '__type' => [ 231 | 'name' => 'Droid', 232 | 'fields' => [ 233 | [ 234 | 'name' => 'id', 235 | 'type' => [ 236 | 'name' => null, 237 | 'kind' => 'NON_NULL', 238 | 'ofType' => [ 239 | 'name' => 'String', 240 | 'kind' => 'SCALAR' 241 | ] 242 | ] 243 | ], 244 | [ 245 | 'name' => 'name', 246 | 'type' => [ 247 | 'name' => 'String', 248 | 'kind' => 'SCALAR', 249 | 'ofType' => null 250 | ] 251 | ], 252 | [ 253 | 'name' => 'friends', 254 | 'type' => [ 255 | 'name' => null, 256 | 'kind' => 'LIST', 257 | 'ofType' => [ 258 | 'name' => 'Character', 259 | 'kind' => 'INTERFACE' 260 | ] 261 | ] 262 | ], 263 | [ 264 | 'name' => 'appearsIn', 265 | 'type' => [ 266 | 'name' => null, 267 | 'kind' => 'LIST', 268 | 'ofType' => [ 269 | 'name' => 'Episode', 270 | 'kind' => 'ENUM' 271 | ] 272 | ] 273 | ], 274 | [ 275 | 'name' => 'primaryFunction', 276 | 'type' => [ 277 | 'name' => 'String', 278 | 'kind' => 'SCALAR', 279 | 'ofType' => null 280 | ] 281 | ] 282 | ] 283 | ] 284 | ]; 285 | $this->assertValidQuery($query, $expected); 286 | } 287 | 288 | public function testAllowsQueryingTheSchemaForFieldArgs(): void 289 | { 290 | $query = ' 291 | query IntrospectionQueryTypeQuery { 292 | __schema { 293 | queryType { 294 | fields { 295 | name 296 | args { 297 | name 298 | description 299 | type { 300 | name 301 | kind 302 | ofType { 303 | name 304 | kind 305 | } 306 | } 307 | defaultValue 308 | } 309 | } 310 | } 311 | } 312 | } 313 | '; 314 | $expected = [ 315 | '__schema' => [ 316 | 'queryType' => [ 317 | 'fields' => [ 318 | [ 319 | 'name' => 'hero', 320 | 'args' => [ 321 | [ 322 | 'defaultValue' => null, 323 | 'description' => "If omitted, returns the hero of the whole saga.\nIf provided, returns the hero of that particular episode.\n", 324 | 'name' => 'episode', 325 | 'type' => [ 326 | 'kind' => 'INPUT_OBJECT', 327 | 'name' => 'HeroInput', 328 | 'ofType' => null, 329 | ], 330 | ], 331 | ], 332 | ], 333 | [ 334 | 'name' => 'human', 335 | 'args' => [ 336 | [ 337 | 'name' => 'id', 338 | 'description' => 'id of the human', 339 | 'type' => [ 340 | 'kind' => 'NON_NULL', 341 | 'name' => null, 342 | 'ofType' => [ 343 | 'kind' => 'SCALAR', 344 | 'name' => 'String', 345 | ], 346 | ], 347 | 'defaultValue' => null, 348 | ], 349 | ], 350 | ], 351 | [ 352 | 'name' => 'droid', 353 | 'args' => [ 354 | [ 355 | 'name' => 'id', 356 | 'description' => 'id of the droid', 357 | 'type' => [ 358 | 'kind' => 'NON_NULL', 359 | 'name' => null, 360 | 'ofType' => 361 | [ 362 | 'kind' => 'SCALAR', 363 | 'name' => 'String', 364 | ], 365 | ], 366 | 'defaultValue' => null, 367 | ], 368 | ], 369 | ], 370 | [ 371 | 'name' => 'dateTime', 372 | 'args' => [ 373 | [ 374 | 'name' => 'dateTime', 375 | 'description' => null, 376 | 'type' => [ 377 | 'name' => 'DateTime', 378 | 'kind' => 'SCALAR', 379 | 'ofType' => null, 380 | ], 381 | 'defaultValue' => null, 382 | ] 383 | ], 384 | ], 385 | ], 386 | ], 387 | ], 388 | ]; 389 | $this->assertValidQuery($query, $expected); 390 | } 391 | 392 | // it('Allows querying the schema for documentation') 393 | public function testAllowsQueryingTheSchemaForDocumentation(): void 394 | { 395 | $query = ' 396 | query IntrospectionDroidDescriptionQuery { 397 | __type(name: "Droid") { 398 | name 399 | description 400 | } 401 | } 402 | '; 403 | $expected = [ 404 | '__type' => [ 405 | 'name' => 'Droid', 406 | 'description' => 'A mechanical creature in the Star Wars universe.' 407 | ] 408 | ]; 409 | $this->assertValidQuery($query, $expected); 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /tests/StarWarsQueryTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Overblog\GraphQLGenerator\Tests; 13 | 14 | class StarWarsQueryTest extends AbstractStarWarsTest 15 | { 16 | // Star Wars Query Tests 17 | // Basic Queries 18 | public function testCorrectlyIdentifiesR2D2AsTheHeroOfTheStarWarsSaga(): void 19 | { 20 | // Correctly identifies R2-D2 as the hero of the Star Wars Saga 21 | $query = ' 22 | query HeroNameQuery { 23 | hero { 24 | name 25 | } 26 | } 27 | '; 28 | $expected = [ 29 | 'hero' => [ 30 | 'name' => 'R2-D2' 31 | ] 32 | ]; 33 | $this->assertValidQuery($query, $expected); 34 | } 35 | 36 | public function testAllowsUsToQueryForTheIDAndFriendsOfR2D2(): void 37 | { 38 | $query = ' 39 | query HeroNameAndFriendsQuery { 40 | hero { 41 | id 42 | name 43 | friends { 44 | name 45 | } 46 | } 47 | } 48 | '; 49 | $expected = [ 50 | 'hero' => [ 51 | 'id' => '2001', 52 | 'name' => 'R2-D2', 53 | 'friends' => [ 54 | [ 55 | 'name' => 'Luke Skywalker', 56 | ], 57 | [ 58 | 'name' => 'Han Solo', 59 | ], 60 | [ 61 | 'name' => 'Leia Organa', 62 | ], 63 | ] 64 | ] 65 | ]; 66 | $this->assertValidQuery($query, $expected); 67 | } 68 | 69 | // Nested Queries 70 | public function testAllowsUsToQueryForTheFriendsOfFriendsOfR2D2(): void 71 | { 72 | $query = ' 73 | query NestedQuery { 74 | hero { 75 | name 76 | friends { 77 | name 78 | appearsIn 79 | friends { 80 | name 81 | } 82 | } 83 | } 84 | } 85 | '; 86 | $expected = [ 87 | 'hero' => [ 88 | 'name' => 'R2-D2', 89 | 'friends' => [ 90 | [ 91 | 'name' => 'Luke Skywalker', 92 | 'appearsIn' => ['NEWHOPE', 'EMPIRE', 'JEDI',], 93 | 'friends' => [ 94 | ['name' => 'Han Solo',], 95 | ['name' => 'Leia Organa',], 96 | ['name' => 'C-3PO',], 97 | ['name' => 'R2-D2',], 98 | ], 99 | ], 100 | [ 101 | 'name' => 'Han Solo', 102 | 'appearsIn' => ['NEWHOPE', 'EMPIRE', 'JEDI'], 103 | 'friends' => [ 104 | ['name' => 'Luke Skywalker',], 105 | ['name' => 'Leia Organa'], 106 | ['name' => 'R2-D2',], 107 | ] 108 | ], 109 | [ 110 | 'name' => 'Leia Organa', 111 | 'appearsIn' => ['NEWHOPE', 'EMPIRE', 'JEDI'], 112 | 'friends' => 113 | [ 114 | ['name' => 'Luke Skywalker',], 115 | ['name' => 'Han Solo',], 116 | ['name' => 'C-3PO',], 117 | ['name' => 'R2-D2',], 118 | ], 119 | ], 120 | ], 121 | ] 122 | ]; 123 | $this->assertValidQuery($query, $expected); 124 | } 125 | 126 | // Using IDs and query parameters to refetch objects 127 | public function testAllowsUsToQueryForLukeSkywalkerDirectlyUsingHisID(): void 128 | { 129 | $query = ' 130 | query FetchLukeQuery { 131 | human(id: "1000") { 132 | name 133 | } 134 | } 135 | '; 136 | $expected = [ 137 | 'human' => [ 138 | 'name' => 'Luke Skywalker' 139 | ] 140 | ]; 141 | $this->assertValidQuery($query, $expected); 142 | } 143 | public function testGenericQueryToGetLukeSkywalkerById(): void 144 | { 145 | // Allows us to create a generic query, then use it to fetch Luke Skywalker using his ID 146 | $query = ' 147 | query FetchSomeIDQuery($someId: String!) { 148 | human(id: $someId) { 149 | name 150 | } 151 | } 152 | '; 153 | $params = [ 154 | 'someId' => '1000' 155 | ]; 156 | $expected = [ 157 | 'human' => [ 158 | 'name' => 'Luke Skywalker' 159 | ] 160 | ]; 161 | $this->assertValidQuery($query, $expected, $params); 162 | } 163 | 164 | public function testGenericQueryToGetHanSoloById(): void 165 | { 166 | // Allows us to create a generic query, then use it to fetch Han Solo using his ID 167 | $query = ' 168 | query FetchSomeIDQuery($someId: String!) { 169 | human(id: $someId) { 170 | name 171 | } 172 | } 173 | '; 174 | $params = [ 175 | 'someId' => '1002' 176 | ]; 177 | $expected = [ 178 | 'human' => [ 179 | 'name' => 'Han Solo' 180 | ] 181 | ]; 182 | $this->assertValidQuery($query, $expected, $params); 183 | } 184 | 185 | public function testGenericQueryWithInvalidId(): void 186 | { 187 | // Allows us to create a generic query, then pass an invalid ID to get null back 188 | $query = ' 189 | query humanQuery($id: String!) { 190 | human(id: $id) { 191 | name 192 | } 193 | } 194 | '; 195 | $params = [ 196 | 'id' => 'not a valid id' 197 | ]; 198 | $expected = [ 199 | 'human' => null 200 | ]; 201 | $this->assertValidQuery($query, $expected, $params); 202 | } 203 | 204 | // Using aliases to change the key in the response 205 | public function testLukeKeyAlias(): void 206 | { 207 | // Allows us to query for Luke, changing his key with an alias 208 | $query = ' 209 | query FetchLukeAliased { 210 | luke: human(id: "1000") { 211 | name 212 | } 213 | } 214 | '; 215 | $expected = [ 216 | 'luke' => [ 217 | 'name' => 'Luke Skywalker' 218 | ], 219 | ]; 220 | $this->assertValidQuery($query, $expected); 221 | } 222 | 223 | public function testTwoRootKeysAsAnAlias(): void 224 | { 225 | // Allows us to query for both Luke and Leia, using two root fields and an alias 226 | $query = ' 227 | query FetchLukeAndLeiaAliased { 228 | luke: human(id: "1000") { 229 | name 230 | } 231 | leia: human(id: "1003") { 232 | name 233 | } 234 | } 235 | '; 236 | $expected = [ 237 | 'luke' => [ 238 | 'name' => 'Luke Skywalker' 239 | ], 240 | 'leia' => [ 241 | 'name' => 'Leia Organa' 242 | ] 243 | ]; 244 | $this->assertValidQuery($query, $expected); 245 | } 246 | 247 | // Uses fragments to express more complex queries 248 | public function testQueryUsingDuplicatedContent(): void 249 | { 250 | // Allows us to query using duplicated content 251 | $query = ' 252 | query DuplicateFields { 253 | luke: human(id: "1000") { 254 | name 255 | homePlanet 256 | } 257 | leia: human(id: "1003") { 258 | name 259 | homePlanet 260 | } 261 | } 262 | '; 263 | $expected = [ 264 | 'luke' => [ 265 | 'name' => 'Luke Skywalker', 266 | 'homePlanet' => 'Tatooine' 267 | ], 268 | 'leia' => [ 269 | 'name' => 'Leia Organa', 270 | 'homePlanet' => 'Alderaan' 271 | ] 272 | ]; 273 | $this->assertValidQuery($query, $expected); 274 | } 275 | 276 | public function testUsingFragment(): void 277 | { 278 | // Allows us to use a fragment to avoid duplicating content 279 | $query = ' 280 | query UseFragment { 281 | luke: human(id: "1000") { 282 | ...HumanFragment 283 | } 284 | leia: human(id: "1003") { 285 | ...HumanFragment 286 | } 287 | } 288 | fragment HumanFragment on Human { 289 | name 290 | homePlanet 291 | } 292 | '; 293 | $expected = [ 294 | 'luke' => [ 295 | 'name' => 'Luke Skywalker', 296 | 'homePlanet' => 'Tatooine' 297 | ], 298 | 'leia' => [ 299 | 'name' => 'Leia Organa', 300 | 'homePlanet' => 'Alderaan' 301 | ] 302 | ]; 303 | $this->assertValidQuery($query, $expected); 304 | } 305 | 306 | // Using __typename to find the type of an object 307 | public function testVerifyThatR2D2IsADroid(): void 308 | { 309 | $query = ' 310 | query CheckTypeOfR2 { 311 | hero { 312 | __typename 313 | name 314 | } 315 | } 316 | '; 317 | $expected = [ 318 | 'hero' => [ 319 | '__typename' => 'Droid', 320 | 'name' => 'R2-D2' 321 | ], 322 | ]; 323 | $this->assertValidQuery($query, $expected); 324 | } 325 | 326 | public function testVerifyThatLukeIsHuman(): void 327 | { 328 | $query = ' 329 | query CheckTypeOfLuke($episode: HeroInput!) { 330 | hero(episode: $episode) { 331 | __typename 332 | name 333 | } 334 | } 335 | '; 336 | $expected = [ 337 | 'hero' => [ 338 | '__typename' => 'Human', 339 | 'name' => 'Luke Skywalker' 340 | ], 341 | ]; 342 | $this->assertValidQuery($query, $expected, ['episode' => ['name' => 'EMPIRE']]); 343 | } 344 | 345 | public function testDateTime(): void 346 | { 347 | $query = '{ dateTime }'; 348 | $expected = [ 349 | 'dateTime' => '2016-11-28 12:00:00', 350 | ]; 351 | $this->assertValidQuery($query, $expected); 352 | 353 | $query = '{ dateTime(dateTime: "2016-01-18 23:00:00") }'; 354 | $expected = [ 355 | 'dateTime' => '2016-01-18 23:00:00', 356 | ]; 357 | $this->assertValidQuery($query, $expected); 358 | $this->assertEquals('The DateTime type', $this->getType('DateTime')->description); 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Overblog\GraphQLGenerator\Tests; 13 | 14 | abstract class TestCase extends \PHPUnit\Framework\TestCase 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /tests/starWarsSchema.yml: -------------------------------------------------------------------------------- 1 | --- 2 | DateTime: 3 | type: custom-scalar 4 | config: 5 | description: "The DateTime type" 6 | serialize: ["Overblog\\GraphQLGenerator\\Tests\\DateTimeType", "serialize"] 7 | parseValue: ["Overblog\\GraphQLGenerator\\Tests\\DateTimeType", "parseValue"] 8 | parseLiteral: ["Overblog\\GraphQLGenerator\\Tests\\DateTimeType", "parseLiteral"] 9 | 10 | # Characters in the Star Wars trilogy are either humans or droids. 11 | # 12 | # This implements the following type system shorthand: 13 | # interface Character { 14 | # id: String! 15 | # name: String 16 | # friends: [Character] 17 | # appearsIn: [Episode] 18 | # } 19 | Character: 20 | type: interface 21 | config: 22 | description: "@='A character' ~ ' in the Star Wars Trilogy'" 23 | fields: 24 | id: 25 | type: "String!" 26 | description: "The id of the character." 27 | name: 28 | type: "String" 29 | description: "The name of the character." 30 | friends: 31 | type: "[Character]" 32 | description: "The friends of the character." 33 | appearsIn: 34 | type: "[Episode]" 35 | description: "Which movies they appear in." 36 | # used expression language to defined resolver (tagged services) 37 | resolveType: "Overblog\\GraphQLGenerator\\Tests\\Resolver::resolveType" 38 | 39 | # The other type of character in Star Wars is a droid. 40 | # 41 | # This implements the following type system shorthand: 42 | # type Droid : Character { 43 | # id: String! 44 | # name: String 45 | # friends: [Character] 46 | # appearsIn: [Episode] 47 | # primaryFunction: String 48 | # } 49 | Droid: 50 | type: object 51 | config: 52 | description: "A mechanical creature in the Star Wars universe." 53 | fields: 54 | id: 55 | type: "String!" 56 | description: "The id of the droid." 57 | resolve: "@=value['id']" 58 | name: 59 | type: "String" 60 | description: "The name of the droid." 61 | friends: 62 | type: "[Character]" 63 | description: "The friends of the droid, or an empty list if they have none." 64 | resolve: ["Overblog\\GraphQLGenerator\\Tests\\Resolver" , "getFriends"] 65 | appearsIn: 66 | type: "[Episode]" 67 | description: "Which movies they appear in." 68 | primaryFunction: 69 | type: "String" 70 | description: "The primary function of the droid." 71 | interfaces: [Character] 72 | 73 | # The original trilogy consists of three movies. 74 | # This implements the following type system shorthand: 75 | # enum Episode { NEWHOPE, EMPIRE, JEDI } 76 | Episode: 77 | type: enum 78 | config: 79 | description: "One of the films in the Star Wars Trilogy" 80 | values: 81 | NEWHOPE: 82 | value: 4 83 | description: "Released in 1977." 84 | EMPIRE: 85 | value: 5 86 | description: "Released in 1980." 87 | JEDI: 88 | value: 6 89 | description: "Released in 1983." 90 | 91 | # We define our human type, which implements the character interface. 92 | # 93 | # This implements the following type system shorthand: 94 | # type Human : Character { 95 | # id: String! 96 | # name: String 97 | # friends: [Character] 98 | # appearsIn: [Episode] 99 | # } 100 | Human: 101 | type: object 102 | config: 103 | description: "A humanoid creature in the Star Wars universe." 104 | fields: 105 | id: 106 | type: "String!" 107 | description: "The id of the character." 108 | name: 109 | type: "String" 110 | description: "The name of the character." 111 | friends: 112 | type: "[Character]" 113 | description: "The friends of the character." 114 | resolve: ["Overblog\\GraphQLGenerator\\Tests\\Resolver" , "getFriends"] 115 | appearsIn: 116 | type: "[Episode]" 117 | description: "Which movies they appear in." 118 | homePlanet: 119 | type: "String" 120 | description: "The home planet of the human, or null if unknown." 121 | interfaces: [Character] 122 | 123 | # This is the type that will be the root of our query, and the 124 | # entry point into our schema. It gives us the ability to fetch 125 | # objects by their IDs, as well as to fetch the undisputed hero 126 | # of the Star Wars trilogy, R2-D2, directly. 127 | # This is the type that will be the root of our query, and the 128 | # entry point into our schema. 129 | # 130 | # This implements the following type system shorthand: 131 | # type Query { 132 | # hero(episode: Episode): Character 133 | # human(id: String!): Human 134 | # droid(id: String!): Droid 135 | # } 136 | # 137 | Query: 138 | type: object 139 | config: 140 | description: "A humanoid creature in the Star Wars universe or a faction in the Star Wars saga." 141 | fields: 142 | hero: 143 | type: "Character" 144 | args: 145 | episode: 146 | type: "HeroInput" 147 | description: | 148 | If omitted, returns the hero of the whole saga. 149 | If provided, returns the hero of that particular episode. 150 | resolve: ["Overblog\\GraphQLGenerator\\Tests\\Resolver", "getHero"] 151 | human: 152 | type: "Human" 153 | args: 154 | id: 155 | description: "id of the human" 156 | type: "String!" 157 | resolve: ["Overblog\\GraphQLGenerator\\Tests\\Resolver", "getHuman"] 158 | droid: 159 | type: "Droid" 160 | args: 161 | id: 162 | description: "id of the droid" 163 | type: "String!" 164 | resolve: ["Overblog\\GraphQLGenerator\\Tests\\Resolver", "getDroid"] 165 | dateTime: 166 | type: "DateTime!" 167 | args: 168 | dateTime: 169 | type: "DateTime" 170 | resolve: ["Overblog\\GraphQLGenerator\\Tests\\Resolver", "getDateTime"] 171 | 172 | HumanAndDroid: 173 | type: union 174 | config: 175 | types: [Human, Droid] 176 | description: Human and Droid 177 | 178 | HeroInput: 179 | type: input-object 180 | config: 181 | fields: 182 | id: 183 | type: "ID" 184 | name: 185 | type: "Episode" 186 | defaultValue: 4 187 | --------------------------------------------------------------------------------