├── .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 | [](https://scrutinizer-ci.com/g/overblog/GraphQLPhpGenerator/?branch=master)
8 | [](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 |
--------------------------------------------------------------------------------