diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 000000000..da2258482 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,2 @@ +coverage_clover: /tmp/coverage/*.xml +json_path: /tmp/coverage/coverage.json diff --git a/.gitattributes b/.gitattributes index 0bddf4164..d04345ede 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,6 +2,7 @@ * text eol=lf /benchmarks export-ignore /tests export-ignore +/examples export-ignore /tools export-ignore .gitattributes export-ignore .gitignore export-ignore diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..0306338fd --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +# These are supported funding model platforms +open_collective: webonyx-graphql-php diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml new file mode 100644 index 000000000..39a4fafea --- /dev/null +++ b/.github/workflows/ci-build.yml @@ -0,0 +1,162 @@ +name: CI + +on: + push: + branches: + tags: + pull_request: + +jobs: + build: + runs-on: ubuntu-18.04 + strategy: + matrix: + php: [7.1, 7.2, 7.3, 7.4, 8.0] + env: [ + 'EXECUTOR= DEPENDENCIES=--prefer-lowest', + 'EXECUTOR=coroutine DEPENDENCIES=--prefer-lowest', + 'EXECUTOR=', + 'EXECUTOR=coroutine', + ] + name: PHP ${{ matrix.php }} Test ${{ matrix.env }} + + steps: + - uses: actions/checkout@v2 + + - name: Install PHP + uses: shivammathur/setup-php@2.9.0 + with: + php-version: ${{ matrix.php }} + coverage: none + extensions: json, mbstring + - name: Get Composer Cache Directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v1 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Remove dependencies not used in this job for PHP 8 compatibility + run: | + composer remove --dev --no-update phpbench/phpbench + composer remove --dev --no-update phpstan/phpstan + composer remove --dev --no-update phpstan/phpstan-phpunit + composer remove --dev --no-update phpstan/phpstan-strict-rules + composer remove --dev --no-update doctrine/coding-standard + + - name: Install Dependencies + run: composer update ${DEPENDENCIES} + + - name: Run unit tests + run: | + export $ENV + ./vendor/bin/phpunit --group default,ReactPromise + env: + ENV: ${{ matrix.env}} + + coding-standard: + runs-on: ubuntu-18.04 + name: Coding Standard + + steps: + - uses: actions/checkout@v2 + + - name: Install PHP + uses: shivammathur/setup-php@2.9.0 + with: + php-version: 7.1 + coverage: none + extensions: json, mbstring + + - name: Get Composer Cache Directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v1 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install Dependencies + run: composer install ${DEPENDENCIES} + + - name: Coding Standard + run: composer lint + + phpstan: + runs-on: ubuntu-18.04 + name: PHPStan + + steps: + - uses: actions/checkout@v2 + + - name: Install PHP + uses: shivammathur/setup-php@2.9.0 + with: + php-version: 7.1 + coverage: none + extensions: json, mbstring + + - name: Get Composer Cache Directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v1 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install Dependencies + run: composer install ${DEPENDENCIES} + + - name: PHPStan + run: composer stan + + coverage: + runs-on: ubuntu-18.04 + name: Code Coverage + + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.ref }} + + - name: Install PHP + uses: shivammathur/setup-php@2.9.0 + with: + php-version: 7.2 + coverage: pcov + extensions: json, mbstring + + - name: Get Composer Cache Directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v1 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install Dependencies + run: composer install ${DEPENDENCIES} + + - name: Code coverage + run: | + ./vendor/bin/phpunit --coverage-clover /tmp/coverage/clover_executor.xml + EXECUTOR=coroutine ./vendor/bin/phpunit --coverage-clover /tmp/coverage/clover_executor-coroutine.xml + + - name: Report to Coveralls + env: + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_RUN_LOCALLY: 1 + run: vendor/bin/php-coveralls --verbose diff --git a/.gitignore b/.gitignore index fa07e1e1f..a83e3c93b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .phpcs-cache +.phpunit.result.cache composer.lock composer.phar phpcs.xml phpstan.neon vendor/ +/.idea diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 9cef7f5f9..000000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,29 +0,0 @@ -build: - nodes: - analysis: - environment: - php: - version: 7.1 - cache: - disabled: false - directories: - - ~/.composer/cache - project_setup: - override: true - tests: - override: - - php-scrutinizer-run - - dependencies: - override: - - composer install --ignore-platform-reqs --no-interaction - -tools: - external_code_coverage: - timeout: 900 - -build_failure_conditions: - - 'elements.rating(<= C).new.exists' # No new classes/methods with a rating of C or worse allowed - - 'issues.label("coding-style").new.exists' # No new coding style issues allowed - - 'issues.severity(>= MAJOR).new.exists' # New issues of major or higher severity - - 'project.metric_change("scrutinizer.test_coverage", < 0)' # Code Coverage decreased from previous inspection diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ac708190c..000000000 --- a/.travis.yml +++ /dev/null @@ -1,66 +0,0 @@ -dist: trusty -language: php - -php: - - 7.1 - - 7.2 - - 7.3 - - 7.4snapshot - - nightly - -env: - matrix: - - EXECUTOR= DEPENDENCIES=--prefer-lowest - - EXECUTOR=coroutine DEPENDENCIES=--prefer-lowest - - EXECUTOR= - - EXECUTOR=coroutine - - -cache: - directories: - - $HOME/.composer/cache - -before_install: - - mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{,.disabled} || echo "xdebug not available" - - travis_retry composer self-update - -install: travis_retry composer update --prefer-dist - -script: ./vendor/bin/phpunit --group default,ReactPromise - -jobs: - allow_failures: - - php: 7.4snapshot - - php: nightly - - include: - - stage: Test - install: - - travis_retry composer update --prefer-dist {$DEPENDENCIES} - - - stage: Test - env: COVERAGE - before_script: - - mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{.disabled,} - - if [[ ! $(php -m | grep -si xdebug) ]]; then echo "xdebug required for coverage"; exit 1; fi - script: - - ./vendor/bin/phpunit --coverage-php /tmp/coverage/clover_executor.cov - - EXECUTOR=coroutine ./vendor/bin/phpunit --coverage-php /tmp/coverage/clover_executor-coroutine.cov - after_script: - - ./vendor/bin/phpcov merge /tmp/coverage --clover /tmp/clover.xml - - wget https://github.com/scrutinizer-ci/ocular/releases/download/1.5.2/ocular.phar - - php ocular.phar code-coverage:upload --format=php-clover /tmp/clover.xml - - - stage: Code Quality - php: 7.1 - env: CODING_STANDARD - install: travis_retry composer install --prefer-dist - script: - - ./vendor/bin/phpcs - - - stage: Code Quality - php: 7.1 - env: STATIC_ANALYSIS - install: travis_retry composer install --prefer-dist - script: composer static-analysis - diff --git a/CHANGELOG.md b/CHANGELOG.md index f501f3e56..72cae2a5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,146 @@ # Changelog + +#### Unreleased + +#### 14.5.1 + +Fix: +- Fix Input Object field shortcut definition with callable (#773) + +#### 14.5.0 + +Feat: +- Implement support for interfaces implementing interfaces (#740), huge kudos to @Kingdutch + +Deprecates: +- Constant `BreakingChangeFinder::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT`. + Use `BreakingChangeFinder::BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED` instead. + Constant value also changed from `INTERFACE_REMOVED_FROM_OBJECT` to `IMPLEMENTED_INTERFACE_REMOVED`. + +- Constant `BreakingChangeFinder::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT` + Use `DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED` instead. + Constant value also changed from `INTERFACE_ADDED_TO_OBJECT` to `IMPLEMENTED_INTERFACE_ADDED`. + +Refactoring: +- Reify AST node types and remove unneeded nullability (#751) + +#### 14.4.1 + +Fix: +- Allow pushing nodes to `NodeList` via `[]=` (#767) +- Fix signature of `Error\FormattedError::prepareFormatter()` to address PHP8 deprecation (#742) +- Do not add errors key to result when errors discarded by custom error handler (#766) + +#### 14.4.0 + +Fix: +- Fixed `SchemaPrinter` so that it uses late static bindings when extended +- Parse `DirectiveDefinitionNode->locations` as `NodeList` (fixes AST::fromArray conversion) (#723) +- Parse `Parser::implementsInterfaces` as `NodeList` (fixes AST::fromArray conversion) +- Fix signature of `Parser::unionMemberTypes` to match actual `NodeList` + +#### v14.3.0 + +Feat: +- Allow `typeLoader` to return a type thunk (#687) + +Fix: +- Read getParsedBody() instead of getBody() when Request is ServerRequest (#715) +- Fix default get/set behavior on InputObjectField and FieldDefinition (#716) + +#### v14.2.0 + +Deprecates: +- Public access to `FieldDefinition::$type` property (#702) + +Fixes: +- Fix validation for input field definition directives (#714) + +#### v14.1.1 + +Fixes: +- Handle nullable `DirectiveNode#astNode` in `SchemaValidationContext` (#708) + +#### v14.1.0 + +New: +- Add partial parse functions for const variants (#693) + +Fixes: +- Differentiate between client-safe and non-client-safe errors in scalar validation (#706) +- Proper type hints for `IntValueNode` (#691) + +Refactoring: +- Ensure NamedTypeNode::$name is always a NameNode (#695) +- Visitor: simplify getVisitFn (#694) +- Replace function calls with type casts (#692) +- Fix "only booleans are allowed" errors (#659) + + +#### v14.0.2 + +- Optimize lazy types (#684) + +#### v14.0.1 + +Bug fixes: +- Fix for: Argument defaults with integer/float values crashes introspection query (#679) +- Fix for "Invalid AST Node: false" error (#685) +- Fix double Error wrapping when parsing variables (#688) + +Refactoring: +- Do not use call_user_func or call_user_func_array (#676) +- Codestyle and static analysis improvements (#648, #690) + +## v14.0.0 + +This release brings several breaking changes. Please refer to [UPGRADE](UPGRADE.md) document for details. + +- **BREAKING/BUGFIX:** Strict coercion of scalar types (#278) +- **BREAKING/BUGFIX:** Spec-compliance: Fixed ambiguity with null variable values and default values (#274) +- **BREAKING:** Removed deprecated directive introspection fields (onOperation, onFragment, onField) +- **BREAKING:** `GraphQL\Deferred` now extends `GraphQL\Executor\Promise\Adapter\SyncPromise` +- **BREAKING:** renamed several types of dangerous/breaking changes (returned by `BreakingChangesFinder`) +- **BREAKING:** Renamed `GraphQL\Error\Debug` to `GraphQL\Error\DebugFlag`. +- **BREAKING:** Debug flags in `GraphQL\Executor\ExecutionResult`, `GraphQL\Error\FormattedError` and `GraphQL\Server\ServerConfig` do not accept `boolean` value anymore but `int` only. +- **BREAKING:** `$positions` in `GraphQL\Error\Error` constructor are not nullable anymore. Same can be expressed by passing an empty array. + +Notable features and improvements: + +- Compliant with the GraphQL specification [June 2018 Edition](https://spec.graphql.org/June2018/) +- Support repeatable directives (#643) +- Perf: support lazy type definitions (#557) +- Simplified Deferred implementation (now allows chaining like promises, #573) +- Support SDL Validation and other schema validation improvements (e.g. #492) +- Added promise adapter for [Amp](https://amphp.org/) (#551) +- Query plan utility improvements (#513, #632) + +Other noteworthy changes: +- Allow retrieving query complexity once query has been completed (#316) +- Allow input types to be passed in from variables using \stdClass instead of associative arrays (#535) +- Support UTF-16 surrogate pairs within string literals (#554, #556) +- Having an empty string in `deprecationReason` will now print the `@deprecated` directive (only a `null` `deprecationReason` won't print the `@deprecated` directive). +- Deprecated Experimental executor (#397) + +Also some bugs fixed, heavily invested in [PHPStan](https://github.com/phpstan/phpstan) for static analysis. + +Special thanks to @simPod, @spawnia and @shmax for their major contributions! + +#### v0.13.9 +- Fix double Error wrapping when parsing variables (#689) + +#### v0.13.8 +- Don't call global field resolver on introspection fields (#481) + +#### v0.13.7 +- Added retrieving query complexity once query has been completed (#316) +- Allow input types to be passed in from variables using \stdClass instead of associative arrays (#535) + +#### v0.13.6 +- QueryPlan can now be used on interfaces not only objects. (#495) +- Array in variables in place of object shouldn't cause fatal error (fixes #467) +- Scalar type ResolverInfo::getFieldSelection support (#529) + #### v0.13.5 - Fix coroutine executor when using with promise (#486) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 399716c8d..2b71dc39d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,8 +10,9 @@ For smaller contributions just use this workflow: * Fork the project. * Add your features and or bug fixes. * Add tests. Tests are important for us. -* Check your changes using `composer check-all` -* Send a pull request +* Check your changes using `composer check`. +* Add an entry to the [Changelog's Unreleases section](CHANGELOG.md#unreleased). +* Send a pull request. ## Setup the Development Environment First, copy the URL of your fork and `git clone` it to your local machine. @@ -50,5 +51,5 @@ Based on [PHPStan](https://github.com/phpstan/phpstan). ## Running benchmarks Benchmarks are run via [PHPBench](https://github.com/phpbench/phpbench). ```sh -./vendor/bin/phpbench run . +./vendor/bin/phpbench run benchmarks ``` diff --git a/README.md b/README.md index 2cf46ba0d..b7702ccca 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # graphql-php -[![Build Status](https://travis-ci.org/webonyx/graphql-php.svg?branch=master)](https://travis-ci.org/webonyx/graphql-php) -[![Code Coverage](https://scrutinizer-ci.com/g/webonyx/graphql-php/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/webonyx/graphql-php) +![CI](https://github.com/webonyx/graphql-php/workflows/CI/badge.svg) +[![Coverage Status](https://coveralls.io/repos/github/webonyx/graphql-php/badge.svg?branch=master)](https://coveralls.io/github/webonyx/graphql-php?branch=master) [![Latest Stable Version](https://poser.pugx.org/webonyx/graphql-php/version)](https://packagist.org/packages/webonyx/graphql-php) [![License](https://poser.pugx.org/webonyx/graphql-php/license)](https://packagist.org/packages/webonyx/graphql-php) @@ -17,12 +17,11 @@ composer require webonyx/graphql-php Full documentation is available on the [Documentation site](https://webonyx.github.io/graphql-php/) as well as in the [docs](docs/) folder of the distribution. -If you don't know what GraphQL is, visit this [official website](http://graphql.org) -by the Facebook engineering team. +If you don't know what GraphQL is, visit the [official website](http://graphql.org) first. ## Examples -There are several ready examples in the [examples](examples/) folder of the distribution with specific -README file per example. +There are several ready examples in the [examples](examples) folder of the distribution, +with a specific README file per example. ## Contributors @@ -49,4 +48,4 @@ Support this project by becoming a sponsor. Your logo will show up here with a l ## License -See [LICENCE](LICENSE). +See [LICENSE](LICENSE). diff --git a/UPGRADE.md b/UPGRADE.md index a9d72560c..23813e60e 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,3 +1,207 @@ +## v0.13.x > v14.x.x + +### BREAKING: Strict coercion of scalar types (#278) + +**Impact: Major** + +This change may break API clients if they were sending loose variable values. + +
+ See Examples + +Consider the following query: + +```graphql +query($intQueryVariable: Int) { + test(intInput: $intQueryVariable) +} +``` + +What happens if we pass non-integer values as `$intQueryVariable`: +``` +[true, false, 1, 0, 0.0, 'true', 'false', '1', '0', '0.0', [], [0,1]] +``` + +#### Integer coercion, changed behavior: + +``` +bool(true): + 0.13.x: coerced to int(1) + 14.x.x: Error: Variable "$queryVariable" got invalid value true; Expected type Int; Int cannot represent non-integer value: true + +bool(false): + 0.13.x: coerced to int(0) + 14.x.x: Error: Variable "$queryVariable" got invalid value false; Expected type Int; Int cannot represent non-integer value: false + +string(1) "1" + 0.13.x: was coerced to int(1) + 14.x.x: Error: Variable "$queryVariable" got invalid value "1"; Expected type Int; Int cannot represent non-integer value: 1 + +string(1) "0" + 0.13.x: was coerced to int(0) + 14.x.x: Error: Variable "$queryVariable" got invalid value "0"; Expected type Int; Int cannot represent non-integer value: 0 + +string(3) "0.0" + 0.13.x: was coerced to int(0) + 14.x.x: Error: Variable "$queryVariable" got invalid value "0.0"; Expected type Int; Int cannot represent non-integer value: 0.0 +``` + +Did not change: +``` +int(1): coerced to int(1) +int(0) was coerced to int(0) +float(0) was coerced to int(0) + +string(4) "true": + Error: Variable "$queryVariable" got invalid value "true"; Expected type Int; Int cannot represent non 32-bit signed integer value: true + +string(5) "false": + Error: Variable "$queryVariable" got invalid value "false"; Expected type Int; Int cannot represent non 32-bit signed integer value: false + +array(0) {} + Error: Variable "$queryVariable" got invalid value []; Expected type Int; Int cannot represent non 32-bit signed integer value: [] + +array(2) { [0]=> int(0) [1]=> int(1) } + Error: Variable "$queryVariable" got invalid value [0,1]; Expected type Int; Int cannot represent non 32-bit signed integer value: [0,1] +``` + +#### Float coercion, changed behavior: +```graphql +query($queryVariable: Float) { + test(floatInput: $queryVariable) +} +``` + +``` +bool(true) + 0.13.x: was coerced to float(1) + 14.x.x: Error: Variable "$queryVariable" got invalid value true; Expected type Float; Float cannot represent non numeric value: true + +bool(false) + 0.13.x: was coerced to float(0) + 14.x.x: Error: Variable "$queryVariable" got invalid value false; Expected type Float; Float cannot represent non numeric value: false + +string(1) "1" + 0.13.x: was coerced to float(1) + 14.x.x: Error: Variable "$queryVariable" got invalid value "1"; Expected type Float; Float cannot represent non numeric value: 1 + +string(1) "0" + 0.13.x: was coerced to float(0) + 14.x.x: Error: Variable "$queryVariable" got invalid value "0"; Expected type Float; Float cannot represent non numeric value: 0 + +string(3) "0.0" + 0.13.x: was coerced to float(0) + 14.x.x: Error: Variable "$queryVariable" got invalid value "0.0"; Expected type Float; Float cannot represent non numeric value: 0.0 +``` + +#### String coercion, changed behavior: +```graphql +query($queryVariable: String) { + test(stringInput: $queryVariable) +} +``` + +``` +bool(true) + 0.13.x: was coerced to string(1) "1" + 14.x.x: Error: Variable "$queryVariable" got invalid value true; Expected type String; String cannot represent a non string value: true + +bool(false) + 0.13.x: was coerced to string(0) "" + 14.x.x: Error: Variable "$queryVariable" got invalid value false; Expected type String; String cannot represent a non string value: false + +int(1) + 0.13.x: was coerced to string(1) "1" + 14.x.x: Error: Variable "$queryVariable" got invalid value 1; Expected type String; String cannot represent a non string value: 1 + +int(0) + 0.13.x: was coerced to string(1) "0" + 14.x.x: Error: Variable "$queryVariable" got invalid value 0; Expected type String; String cannot represent a non string value: 0 + +float(0) + 0.13.x: was coerced to string(1) "0" + 14.x.x: Error: Variable "$queryVariable" got invalid value 0; Expected type String; String cannot represent a non string value: 0 +``` + +#### Boolean coercion did not change. + +
+ +### Breaking: renamed classes and changed signatures + +**Impact: Medium** + +- Dropped previously deprecated `GraphQL\Schema`. Use `GraphQL\Type\Schema`. +- Renamed `GraphQL\Error\Debug` to `GraphQL\Error\DebugFlag`. +- Debug flags in `GraphQL\Executor\ExecutionResult`, `GraphQL\Error\FormattedError` and `GraphQL\Server\ServerConfig` + do not accept `boolean` value anymore but `int` only (pass values of `GraphQL\Error\DebugFlag` constants) +- `$positions` in `GraphQL\Error\Error` are not nullable anymore. Same can be expressesed by passing empty array. + +### BREAKING: Removed deprecated directive introspection fields (onOperation, onFragment, onField) + +**Impact: Minor** + +Could affect developer tools relying on old introspection format. +Replaced with [Directive Locations](https://spec.graphql.org/June2018/#sec-Type-System.Directives). + +### BREAKING: Changes in validation rules: + +**Impact: Minor** + + - Removal of `VariablesDefaultValueAllowed` validation rule. All variables may now specify a default value. + - Renamed `ProvidedNonNullArguments` to `ProvidedRequiredArguments` (no longer require values to be provided to non-null arguments which provide a default value). + +Could affect projects using custom sets of validation rules. + +### BREAKING: `GraphQL\Deferred` now extends `GraphQL\Executor\Promise\Adapter\SyncPromise` + +**Impact: Minor** + +Can only affect a few projects that were somehow customizing deferreds or the default sync promise adapter. + + +### BREAKING: renamed several types of dangerous/breaking changes (returned by `BreakingChangesFinder`): + +**Impact: Minor** + +Can affect projects relying on `BreakingChangesFinder` utility in their CI. + +Following types of changes were renamed: + +``` +- `NON_NULL_ARG_ADDED` to `REQUIRED_ARG_ADDED` +- `NON_NULL_INPUT_FIELD_ADDED` to `REQUIRED_INPUT_FIELD_ADDED` +- `NON_NULL_DIRECTIVE_ARG_ADDED` to `REQUIRED_DIRECTIVE_ARG_ADDED` +- `NULLABLE_INPUT_FIELD_ADDED` to `OPTIONAL_INPUT_FIELD_ADDED` +- `NULLABLE_ARG_ADDED` to `OPTIONAL_ARG_ADDED` +``` + +### Breaking: Dropped `GraphQL\Error\Error::$message` + +**Impact: Minor** + +Use `GraphQL\Error\Error->getMessage()` instead. + +### Breaking: change TypeKind constants + +**Impact: Minor** + +The constants in `\GraphQL\Type\TypeKind` were partly renamed and their values +have been changed to match their name instead of a numeric index. + +### Breaking: some error messages were changed + +**Impact: Minor** + +Can affect projects relying on error messages parsing. + +One example: added quotes around `parentType.fieldName` in error message: +```diff +- Cannot return null for non-nullable field parentType.fieldName. ++ Cannot return null for non-nullable field "parentType.fieldName". +``` +But expect other simiar changes like this. + ## Upgrade v0.12.x > v0.13.x ### Breaking (major): minimum supported version of PHP diff --git a/benchmarks/Utils/SchemaGenerator.php b/benchmarks/Utils/SchemaGenerator.php index 8ee39e7e3..1dce8f68d 100644 --- a/benchmarks/Utils/SchemaGenerator.php +++ b/benchmarks/Utils/SchemaGenerator.php @@ -152,7 +152,7 @@ protected function createFieldArgs($fieldName, $typeName) ]; } - public function resolveField($value, $args, $context, $resolveInfo) + public function resolveField($objectValue, $args, $context, $resolveInfo) { return $resolveInfo->fieldName . '-value'; } diff --git a/benchmarks/shim.php b/benchmarks/shim.php new file mode 100644 index 000000000..6a80fa29d --- /dev/null +++ b/benchmarks/shim.php @@ -0,0 +1,22 @@ +setUp(); +$b->benchSchema(); +$b->benchSchemaLazy(); +$b->benchSmallQuery(); +$b->benchSmallQueryLazy(); + +$b = new \GraphQL\Benchmarks\LexerBench(); +$b->setUp(); +$b->benchIntrospectionQuery(); + +$b = new \GraphQL\Benchmarks\StarWarsBench(); +$b->setIntroQuery(); +$b->benchSchema(); +$b->benchHeroQuery(); +$b->benchNestedQuery(); +$b->benchQueryWithFragment(); +$b->benchStarWarsIntrospectionQuery(); diff --git a/composer.json b/composer.json index 113e79d92..ae42f0099 100644 --- a/composer.json +++ b/composer.json @@ -14,15 +14,19 @@ "ext-mbstring": "*" }, "require-dev": { + "amphp/amp": "^2.3", "doctrine/coding-standard": "^6.0", - "phpbench/phpbench": "^0.14.0", - "phpstan/phpstan": "^0.11.4", - "phpstan/phpstan-phpunit": "^0.11.0", - "phpstan/phpstan-strict-rules": "^0.11.0", - "phpunit/phpcov": "^5.0", - "phpunit/phpunit": "^7.2", + "nyholm/psr7": "^1.2", + "phpbench/phpbench": "^0.16.10", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "0.12.32", + "phpstan/phpstan-phpunit": "0.12.11", + "phpstan/phpstan-strict-rules": "0.12.2", + "phpunit/phpunit": "^7.2|^8.5", "psr/http-message": "^1.0", - "react/promise": "2.*" + "react/promise": "2.*", + "simpod/php-coveralls-mirror": "^3.0", + "squizlabs/php_codesniffer": "3.5.4" }, "config": { "preferred-install": "dist", @@ -49,8 +53,9 @@ "bench": "phpbench run .", "test": "phpunit", "lint" : "phpcs", - "fix-style" : "phpcbf", - "static-analysis": "phpstan analyse --ansi --memory-limit 256M", - "check-all": "composer lint && composer static-analysis && composer test" + "fix" : "phpcbf", + "stan": "phpstan analyse --ansi --memory-limit 2048M", + "baseline": "phpstan analyse --ansi --generate-baseline", + "check": "composer lint && composer stan && composer test" } } diff --git a/docs/best-practices.md b/docs/best-practices.md index a5f135eb5..4b7fc247b 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -1,13 +1,3 @@ -# Config Validation -Defining types using arrays may be error-prone, but **graphql-php** provides config validation -tool to report when config has unexpected structure. - -This validation tool is **disabled by default** because it is time-consuming operation which only -makes sense during development. - -To enable validation - call: `GraphQL\Type\Definition\Config::enableValidation();` in your bootstrap -but make sure to restrict it to debug/development mode only. - # Type Registry **graphql-php** expects that each type in Schema is presented by single instance. Therefore if you define your types as separate PHP classes you need to ensure that each type is referenced only once. @@ -16,4 +6,4 @@ Technically you can create several instances of your type (for example for tests will throw on attempt to add different instances with the same name. There are several ways to achieve this depending on your preferences. We provide reference -implementation below that introduces TypeRegistry class: \ No newline at end of file +implementation below that introduces TypeRegistry class: diff --git a/docs/complementary-tools.md b/docs/complementary-tools.md index 4710ebd9a..5262649e7 100644 --- a/docs/complementary-tools.md +++ b/docs/complementary-tools.md @@ -3,8 +3,10 @@ * [Standard Server](executing-queries.md/#using-server) – Out of the box integration with any PSR-7 compatible framework (like [Slim](http://slimframework.com) or [Zend Expressive](http://zendframework.github.io/zend-expressive/)). * [Relay Library for graphql-php](https://github.com/ivome/graphql-relay-php) – Helps construct Relay related schema definitions. * [Lighthouse](https://github.com/nuwave/lighthouse) – Laravel based, uses Schema Definition Language +* [Laravel GraphQL](https://github.com/rebing/graphql-laravel) - Laravel wrapper for Facebook's GraphQL * [OverblogGraphQLBundle](https://github.com/overblog/GraphQLBundle) – Bundle for Symfony * [WP-GraphQL](https://github.com/wp-graphql/wp-graphql) - GraphQL API for WordPress +* [Siler](https://github.com/leocavalcante/siler) - Straightforward way to map GraphQL SDL to resolver callables, also built-in support for Swoole # GraphQL PHP Tools @@ -23,3 +25,4 @@ * [ChromeiQL](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij) or [GraphiQL Feen](https://chrome.google.com/webstore/detail/graphiql-feen/mcbfdonlkfpbfdpimkjilhdneikhfklp) – GraphiQL as Google Chrome extension +* [Altair GraphQL Client](https://altair.sirmuel.design/) - A beautiful feature-rich GraphQL Client for all platforms diff --git a/docs/data-fetching.md b/docs/data-fetching.md index 4bc0cfbd8..c5152ba5f 100644 --- a/docs/data-fetching.md +++ b/docs/data-fetching.md @@ -103,23 +103,25 @@ for a field you simply override this default resolver. **graphql-php** provides following default field resolver: ```php fieldName; - $property = null; - - if (is_array($source) || $source instanceof \ArrayAccess) { - if (isset($source[$fieldName])) { - $property = $source[$fieldName]; +function defaultFieldResolver($objectValue, $args, $context, \GraphQL\Type\Definition\ResolveInfo $info) + { + $fieldName = $info->fieldName; + $property = null; + + if (is_array($objectValue) || $objectValue instanceof \ArrayAccess) { + if (isset($objectValue[$fieldName])) { + $property = $objectValue[$fieldName]; + } + } elseif (is_object($objectValue)) { + if (isset($objectValue->{$fieldName})) { + $property = $objectValue->{$fieldName}; + } } - } else if (is_object($source)) { - if (isset($source->{$fieldName})) { - $property = $source->{$fieldName}; - } - } - return $property instanceof Closure ? $property($source, $args, $context, $info) : $property; -} + return $property instanceof Closure + ? $property($objectValue, $args, $context, $info) + : $property; + } ``` As you see it returns value by key (for arrays) or property (for objects). @@ -161,7 +163,6 @@ $userType = new ObjectType([ Keep in mind that **field resolver** has precedence over **default field resolver per type** which in turn has precedence over **default field resolver**. - # Solving N+1 Problem Since: 0.9.0 diff --git a/docs/error-handling.md b/docs/error-handling.md index 17b0a2a4e..c3841b21e 100644 --- a/docs/error-handling.md +++ b/docs/error-handling.md @@ -17,7 +17,9 @@ By default, each error entry is converted to an associative array with following 'Error message', - 'category' => 'graphql', + 'extensions' => [ + 'category' => 'graphql' + ], 'locations' => [ ['line' => 1, 'column' => 2] ], @@ -67,7 +69,9 @@ When such exception is thrown it will be reported with a full error message: 'My reported error', - 'category' => 'businessLogic', + 'extensions' => [ + 'category' => 'businessLogic' + ], 'locations' => [ ['line' => 10, 'column' => 2] ], @@ -86,12 +90,12 @@ GraphQL\Error\FormattedError::setInternalErrorMessage("Unexpected error"); # Debugging tools -During development or debugging use `$result->toArray(true)` to add **debugMessage** key to +During development or debugging use `$result->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)` to add **debugMessage** key to each formatted error entry. If you also want to add exception trace - pass flags instead: -``` -use GraphQL\Error\Debug; -$debug = Debug::INCLUDE_DEBUG_MESSAGE | Debug::INCLUDE_TRACE; +```php +use GraphQL\Error\DebugFlag; +$debug = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE; $result = GraphQL::executeQuery(/*args*/)->toArray($debug); ``` @@ -101,7 +105,9 @@ This will make each error entry to look like this: [ 'debugMessage' => 'Actual exception message', 'message' => 'Internal server error', - 'category' => 'internal', + 'extensions' => [ + 'category' => 'internal' + ], 'locations' => [ ['line' => 10, 'column' => 2] ], @@ -120,8 +126,8 @@ If you prefer the first resolver exception to be re-thrown, use following flags: ```php toArray($debug); diff --git a/docs/executing-queries.md b/docs/executing-queries.md index 5cfea251d..54f82da03 100644 --- a/docs/executing-queries.md +++ b/docs/executing-queries.md @@ -1,8 +1,8 @@ # Using Facade Method -Query execution is a complex process involving multiple steps, including query **parsing**, +Query execution is a complex process involving multiple steps, including query **parsing**, **validating** and finally **executing** against your [schema](type-system/schema.md). -**graphql-php** provides a convenient facade for this process in class +**graphql-php** provides a convenient facade for this process in class [`GraphQL\GraphQL`](reference.md#graphqlgraphql): ```php @@ -10,26 +10,26 @@ Query execution is a complex process involving multiple steps, including query * use GraphQL\GraphQL; $result = GraphQL::executeQuery( - $schema, - $queryString, - $rootValue = null, - $context = null, - $variableValues = null, + $schema, + $queryString, + $rootValue = null, + $context = null, + $variableValues = null, $operationName = null, $fieldResolver = null, $validationRules = null ); ``` -It returns an instance of [`GraphQL\Executor\ExecutionResult`](reference.md#graphqlexecutorexecutionresult) +It returns an instance of [`GraphQL\Executor\ExecutionResult`](reference.md#graphqlexecutorexecutionresult) which can be easily converted to array: ```php $serializableResult = $result->toArray(); ``` -Returned array contains **data** and **errors** keys, as described by the -[GraphQL spec](http://facebook.github.io/graphql/#sec-Response-Format). +Returned array contains **data** and **errors** keys, as described by the +[GraphQL spec](http://facebook.github.io/graphql/#sec-Response-Format). This array is suitable for further serialization (e.g. using **json_encode**). See also the section on [error handling and formatting](error-handling.md). @@ -41,14 +41,14 @@ schema | [`GraphQL\Type\Schema`](#) | **Required.** Instance of your appli queryString | `string` or `GraphQL\Language\AST\DocumentNode` | **Required.** Actual GraphQL query string to be parsed, validated and executed. If you parse query elsewhere before executing - pass corresponding AST document here to avoid new parsing. rootValue | `mixed` | Any value that represents a root of your data graph. It is passed as the 1st argument to field resolvers of [Query type](type-system/schema.md#query-and-mutation-types). Can be omitted or set to null if actual root values are fetched by Query type itself. context | `mixed` | Any value that holds information shared between all field resolvers. Most often they use it to pass currently logged in user, locale details, etc.

It will be available as the 3rd argument in all field resolvers. (see section on [Field Definitions](type-system/object-types.md#field-configuration-options) for reference) **graphql-php** never modifies this value and passes it *as is* to all underlying resolvers. -variableValues | `array` | Map of variable values passed along with query string. See section on [query variables on official GraphQL website](http://graphql.org/learn/queries/#variables) +variableValues | `array` | Map of variable values passed along with query string. See section on [query variables on official GraphQL website](http://graphql.org/learn/queries/#variables). Note that while variableValues must be an associative array, the values inside it can be nested using \stdClass if desired. operationName | `string` | Allows the caller to specify which operation in queryString will be run, in cases where queryString contains multiple top-level operations. fieldResolver | `callable` | A resolver function to use when one is not provided by the schema. If not provided, the [default field resolver is used](data-fetching.md#default-field-resolver). validationRules | `array` | A set of rules for query validation step. The default value is all available rules. Empty array would allow skipping query validation (may be convenient for persisted queries which are validated before persisting and assumed valid during execution) # Using Server -If you are building HTTP GraphQL API, you may prefer our Standard Server -(compatible with [express-graphql](https://github.com/graphql/express-graphql)). +If you are building HTTP GraphQL API, you may prefer our Standard Server +(compatible with [express-graphql](https://github.com/graphql/express-graphql)). It supports more features out of the box, including parsing HTTP requests, producing a spec-compliant response; [batched queries](#query-batching); persisted queries. Usage example (with plain PHP): @@ -66,11 +66,11 @@ Server also supports [PSR-7 request/response interfaces](http://www.php-fig.org/
The server does not implement persistence part (which you will have to build on your own), but it allows you to execute queries which were persisted previously.

Expected function signature:
**function ($queryId, [OperationParams](reference.md#graphqlserveroperationparams) $params)**

Function is expected to return query **string** or parsed **DocumentNode**

[Read more about persisted queries](https://dev-blog.apollodata.com/persisted-graphql-queries-with-apollo-client-119fd7e6bba5). errorFormatter | `callable` | Custom error formatter. See [error handling docs](error-handling.md#custom-error-handling-and-formatting). errorsHandler | `callable` | Custom errors handler. See [error handling docs](error-handling.md#custom-error-handling-and-formatting). -promiseAdapter | [`PromiseAdapter`](reference.md#graphqlexecutorpromisepromiseadapter) | Required for [Async PHP](data-fetching/#async-php) only. +promiseAdapter | [`PromiseAdapter`](reference.md#graphqlexecutorpromisepromiseadapter) | Required for [Async PHP](data-fetching/#async-php) only. **Server config instance** -If you prefer fluid interface for config with autocomplete in IDE and static time validation, +If you prefer fluid interface for config with autocomplete in IDE and static time validation, use [`GraphQL\Server\ServerConfig`](reference.md#graphqlserverserverconfig) instead of an array: ```php @@ -120,7 +120,7 @@ use GraphQL\Server\StandardServer; $config = ServerConfig::create() ->setSchema($schema) ->setErrorFormatter($myFormatter) - ->setDebug($debug) + ->setDebugFlag($debug) ; $server = new StandardServer($config); @@ -129,7 +129,7 @@ $server = new StandardServer($config); ## Query batching Standard Server supports query batching ([apollo-style](https://dev-blog.apollodata.com/query-batching-in-apollo-63acfd859862)). -One of the major benefits of Server over a sequence of **executeQuery()** calls is that +One of the major benefits of Server over a sequence of **executeQuery()** calls is that [Deferred resolvers](data-fetching.md#solving-n1-problem) won't be isolated in queries. So for example following batch will require single DB request (if user field is deferred): @@ -186,11 +186,11 @@ $myValiationRules = array_merge( ); $result = GraphQL::executeQuery( - $schema, - $queryString, - $rootValue = null, - $context = null, - $variableValues = null, + $schema, + $queryString, + $rootValue = null, + $context = null, + $variableValues = null, $operationName = null, $fieldResolver = null, $myValiationRules // <-- this will override global validation rules for this request @@ -199,7 +199,7 @@ $result = GraphQL::executeQuery( Or with a standard server: ```php - [ 'message' => Type::nonNull(Type::string()), ], - 'resolve' => function ($root, $args) { - return $root['prefix'] . $args['message']; + 'resolve' => function ($rootValue, $args) { + return $rootValue['prefix'] . $args['message']; } ], ], diff --git a/docs/reference.md b/docs/reference.md index 7ea770950..73f811481 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -20,9 +20,11 @@ See [related documentation](executing-queries.md). * rootValue: * The value provided as the first argument to resolver functions on the top * level type (e.g. the query object type). - * context: - * The value provided as the third argument to all resolvers. - * Use this to pass current session, user data, etc + * contextValue: + * The context value is provided as an argument to resolver functions after + * field arguments. It is used to pass shared information useful at any point + * during executing this query, for example the currently logged in user and + * connections to databases or other services. * variableValues: * A mapping of variable name to runtime value to use for all variables * defined in the requestString. @@ -41,7 +43,7 @@ See [related documentation](executing-queries.md). * * @param string|DocumentNode $source * @param mixed $rootValue - * @param mixed $context + * @param mixed $contextValue * @param mixed[]|null $variableValues * @param ValidationRule[] $validationRules * @@ -51,12 +53,12 @@ static function executeQuery( GraphQL\Type\Schema $schema, $source, $rootValue = null, - $context = null, + $contextValue = null, $variableValues = null, string $operationName = null, callable $fieldResolver = null, array $validationRules = null -) +): GraphQL\Executor\ExecutionResult ``` ```php @@ -82,7 +84,7 @@ static function promiseToExecute( string $operationName = null, callable $fieldResolver = null, array $validationRules = null -) +): GraphQL\Executor\Promise\Promise ``` ```php @@ -93,7 +95,7 @@ static function promiseToExecute( * * @api */ -static function getStandardDirectives() +static function getStandardDirectives(): array ``` ```php @@ -104,7 +106,7 @@ static function getStandardDirectives() * * @api */ -static function getStandardTypes() +static function getStandardTypes(): array ``` ```php @@ -112,7 +114,7 @@ static function getStandardTypes() * Replaces standard types with types from this list (matching by name) * Standard types not listed here remain untouched. * - * @param Type[] $types + * @param array $types * * @api */ @@ -127,7 +129,7 @@ static function overrideStandardTypes(array $types) * * @api */ -static function getStandardValidationRules() +static function getStandardValidationRules(): array ``` ```php @@ -136,7 +138,7 @@ static function getStandardValidationRules() * * @api */ -static function setDefaultFieldResolver(callable $fn) +static function setDefaultFieldResolver(callable $fn): void ``` # GraphQL\Type\Definition\Type Registry of standard GraphQL types @@ -145,198 +147,164 @@ and a base class for all other types. **Class Methods:** ```php /** - * @return IDType - * * @api */ -static function id() +static function id(): GraphQL\Type\Definition\ScalarType ``` ```php /** - * @return StringType - * * @api */ -static function string() +static function string(): GraphQL\Type\Definition\ScalarType ``` ```php /** - * @return BooleanType - * * @api */ -static function boolean() +static function boolean(): GraphQL\Type\Definition\ScalarType ``` ```php /** - * @return IntType - * * @api */ -static function int() +static function int(): GraphQL\Type\Definition\ScalarType ``` ```php /** - * @return FloatType - * * @api */ -static function float() +static function float(): GraphQL\Type\Definition\ScalarType ``` ```php /** - * @param Type|ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType - * - * @return ListOfType - * * @api */ -static function listOf($wrappedType) +static function listOf(GraphQL\Type\Definition\Type $wrappedType): GraphQL\Type\Definition\ListOfType ``` ```php /** - * @param ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType $wrappedType - * - * @return NonNull + * @param callable|NullableType $wrappedType * * @api */ -static function nonNull($wrappedType) +static function nonNull($wrappedType): GraphQL\Type\Definition\NonNull ``` ```php /** * @param Type $type * - * @return bool - * * @api */ -static function isInputType($type) +static function isInputType($type): bool ``` ```php /** * @param Type $type * - * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType - * * @api */ -static function getNamedType($type) +static function getNamedType($type): GraphQL\Type\Definition\Type ``` ```php /** * @param Type $type * - * @return bool - * * @api */ -static function isOutputType($type) +static function isOutputType($type): bool ``` ```php /** * @param Type $type * - * @return bool - * * @api */ -static function isLeafType($type) +static function isLeafType($type): bool ``` ```php /** * @param Type $type * - * @return bool - * * @api */ -static function isCompositeType($type) +static function isCompositeType($type): bool ``` ```php /** * @param Type $type * - * @return bool - * * @api */ -static function isAbstractType($type) +static function isAbstractType($type): bool ``` ```php /** - * @param Type $type - * - * @return bool - * * @api */ -static function isType($type) +static function getNullableType(GraphQL\Type\Definition\Type $type): GraphQL\Type\Definition\Type ``` +# GraphQL\Type\Definition\ResolveInfo +Structure containing information useful for field resolution process. + +Passed as 4th argument to every field resolver. See [docs on field resolving (data fetching)](data-fetching.md). +**Class Props:** ```php /** - * @param Type $type - * - * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType + * The definition of the field being resolved. * * @api + * @var FieldDefinition */ -static function getNullableType($type) -``` -# GraphQL\Type\Definition\ResolveInfo -Structure containing information useful for field resolution process. -Passed as 3rd argument to every field resolver. See [docs on field resolving (data fetching)](data-fetching.md). +public $fieldDefinition; -**Class Props:** -```php /** - * The name of the field being resolved + * The name of the field being resolved. * * @api - * @var string|null + * @var string */ public $fieldName; /** - * AST of all nodes referencing this field in the query. + * Expected return type of the field being resolved. * * @api - * @var FieldNode[]|null + * @var Type */ -public $fieldNodes; +public $returnType; /** - * Expected return type of the field being resolved + * AST of all nodes referencing this field in the query. * * @api - * @var ScalarType|ObjectType|InterfaceType|UnionType|EnumType|ListOfType|NonNull + * @var FieldNode[] */ -public $returnType; +public $fieldNodes; /** - * Parent type of the field being resolved + * Parent type of the field being resolved. * * @api - * @var ObjectType|null + * @var ObjectType */ public $parentType; /** - * Path to this field from the very root value + * Path to this field from the very root value. * * @api * @var string[] @@ -344,31 +312,31 @@ public $parentType; public $path; /** - * Instance of a schema used for execution + * Instance of a schema used for execution. * * @api - * @var Schema|null + * @var Schema */ public $schema; /** - * AST of all fragments defined in query + * AST of all fragments defined in query. * * @api - * @var FragmentDefinitionNode[]|null + * @var FragmentDefinitionNode[] */ public $fragments; /** - * Root value passed to query execution + * Root value passed to query execution. * * @api - * @var mixed|null + * @var mixed */ public $rootValue; /** - * AST of operation definition node (query, mutation) + * AST of operation definition node (query, mutation). * * @api * @var OperationDefinitionNode|null @@ -376,10 +344,10 @@ public $rootValue; public $operation; /** - * Array of variables passed to query execution + * Array of variables passed to query execution. * * @api - * @var mixed[]|null + * @var mixed[] */ public $variableValues; ``` @@ -388,7 +356,7 @@ public $variableValues; ```php /** * Helper method that returns names of all fields selected in query for - * $this->fieldName up to $depth levels + * $this->fieldName up to $depth levels. * * Example: * query MyQuery{ @@ -419,7 +387,7 @@ public $variableValues; * * @param int $depth How many levels to include in output * - * @return bool[] + * @return array * * @api */ @@ -437,6 +405,7 @@ const FIELD = "FIELD"; const FRAGMENT_DEFINITION = "FRAGMENT_DEFINITION"; const FRAGMENT_SPREAD = "FRAGMENT_SPREAD"; const INLINE_FRAGMENT = "INLINE_FRAGMENT"; +const VARIABLE_DEFINITION = "VARIABLE_DEFINITION"; const SCHEMA = "SCHEMA"; const SCALAR = "SCALAR"; const OBJECT = "OBJECT"; @@ -480,7 +449,7 @@ static function create(array $options = []) ```php /** - * @return ObjectType + * @return ObjectType|null * * @api */ @@ -489,7 +458,7 @@ function getQuery() ```php /** - * @param ObjectType $query + * @param ObjectType|null $query * * @return SchemaConfig * @@ -500,7 +469,7 @@ function setQuery($query) ```php /** - * @return ObjectType + * @return ObjectType|null * * @api */ @@ -509,7 +478,7 @@ function getMutation() ```php /** - * @param ObjectType $mutation + * @param ObjectType|null $mutation * * @return SchemaConfig * @@ -520,7 +489,7 @@ function setMutation($mutation) ```php /** - * @return ObjectType + * @return ObjectType|null * * @api */ @@ -529,7 +498,7 @@ function getSubscription() ```php /** - * @param ObjectType $subscription + * @param ObjectType|null $subscription * * @return SchemaConfig * @@ -540,7 +509,7 @@ function setSubscription($subscription) ```php /** - * @return Type[] + * @return Type[]|callable * * @api */ @@ -560,7 +529,7 @@ function setTypes($types) ```php /** - * @return Directive[] + * @return Directive[]|null * * @api */ @@ -580,7 +549,7 @@ function setDirectives(array $directives) ```php /** - * @return callable + * @return callable(string $name):Type|null * * @api */ @@ -658,7 +627,7 @@ function getDirectives() * * @api */ -function getQueryType() +function getQueryType(): GraphQL\Type\Definition\Type ``` ```php @@ -669,7 +638,7 @@ function getQueryType() * * @api */ -function getMutationType() +function getMutationType(): GraphQL\Type\Definition\Type ``` ```php @@ -680,7 +649,7 @@ function getMutationType() * * @api */ -function getSubscriptionType() +function getSubscriptionType(): GraphQL\Type\Definition\Type ``` ```php @@ -694,15 +663,11 @@ function getConfig() ```php /** - * Returns type by it's name - * - * @param string $name - * - * @return Type|null + * Returns type by its name * * @api */ -function getType($name) +function getType(string $name): GraphQL\Type\Definition\Type ``` ```php @@ -712,11 +677,13 @@ function getType($name) * * This operation requires full schema scan. Do not use in production environment. * - * @return ObjectType[] + * @param InterfaceType|UnionType $abstractType + * + * @return array * * @api */ -function getPossibleTypes(GraphQL\Type\Definition\AbstractType $abstractType) +function getPossibleTypes(GraphQL\Type\Definition\Type $abstractType): array ``` ```php @@ -724,27 +691,21 @@ function getPossibleTypes(GraphQL\Type\Definition\AbstractType $abstractType) * Returns true if object type is concrete type of given abstract type * (implementation for interfaces and members of union type for unions) * - * @return bool - * * @api */ function isPossibleType( GraphQL\Type\Definition\AbstractType $abstractType, GraphQL\Type\Definition\ObjectType $possibleType -) +): bool ``` ```php /** * Returns instance of directive by name * - * @param string $name - * - * @return Directive - * * @api */ -function getDirective($name) +function getDirective(string $name): GraphQL\Type\Definition\Directive ``` ```php @@ -775,6 +736,42 @@ function validate() # GraphQL\Language\Parser Parses string containing GraphQL query or [type definition](type-system/type-language.md) to Abstract Syntax Tree. +Those magic functions allow partial parsing: + +@method static DocumentNode document(Source|string $source, bool[] $options = []) +@method static ExecutableDefinitionNode executableDefinition(Source|string $source, bool[] $options = []) +@method static string operationType(Source|string $source, bool[] $options = []) +@method static VariableDefinitionNode variableDefinition(Source|string $source, bool[] $options = []) +@method static SelectionSetNode selectionSet(Source|string $source, bool[] $options = []) +@method static FieldNode field(Source|string $source, bool[] $options = []) +@method static NodeList constArguments(Source|string $source, bool[] $options = []) +@method static ArgumentNode constArgument(Source|string $source, bool[] $options = []) +@method static FragmentDefinitionNode fragmentDefinition(Source|string $source, bool[] $options = []) +@method static BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|NullValueNode|ObjectValueNode|StringValueNode|VariableNode valueLiteral(Source|string $source, bool[] $options = []) +@method static StringValueNode stringLiteral(Source|string $source, bool[] $options = []) +@method static BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|ObjectValueNode|StringValueNode|VariableNode variableValue(Source|string $source, bool[] $options = []) +@method static ListValueNode constArray(Source|string $source, bool[] $options = []) +@method static ObjectValueNode constObject(Source|string $source, bool[] $options = []) +@method static ObjectFieldNode constObjectField(Source|string $source, bool[] $options = []) +@method static NodeList constDirectives(Source|string $source, bool[] $options = []) +@method static DirectiveNode constDirective(Source|string $source, bool[] $options = []) +@method static NamedTypeNode namedType(Source|string $source, bool[] $options = []) +@method static StringValueNode|null description(Source|string $source, bool[] $options = []) +@method static OperationTypeDefinitionNode operationTypeDefinition(Source|string $source, bool[] $options = []) +@method static ObjectTypeDefinitionNode objectTypeDefinition(Source|string $source, bool[] $options = []) +@method static NodeList fieldsDefinition(Source|string $source, bool[] $options = []) +@method static NodeList argumentsDefinition(Source|string $source, bool[] $options = []) +@method static InterfaceTypeDefinitionNode interfaceTypeDefinition(Source|string $source, bool[] $options = []) +@method static NamedTypeNode[] unionMemberTypes(Source|string $source, bool[] $options = []) +@method static NodeList enumValuesDefinition(Source|string $source, bool[] $options = []) +@method static InputObjectTypeDefinitionNode inputObjectTypeDefinition(Source|string $source, bool[] $options = []) +@method static TypeExtensionNode typeExtension(Source|string $source, bool[] $options = []) +@method static ScalarTypeExtensionNode scalarTypeExtension(Source|string $source, bool[] $options = []) +@method static InterfaceTypeExtensionNode interfaceTypeExtension(Source|string $source, bool[] $options = []) +@method static EnumTypeExtensionNode enumTypeExtension(Source|string $source, bool[] $options = []) +@method static DirectiveDefinitionNode directiveDefinition(Source|string $source, bool[] $options = []) +@method static DirectiveLocation directiveLocation(Source|string $source, bool[] $options = []) + **Class Methods:** ```php /** @@ -865,7 +862,7 @@ static function parseValue($source, array $options = []) * @param Source|string $source * @param bool[] $options * - * @return ListTypeNode|NameNode|NonNullTypeNode + * @return ListTypeNode|NamedTypeNode|NonNullTypeNode * * @api */ @@ -1095,7 +1092,7 @@ Implements the "Evaluating requests" section of the GraphQL specification. * execution are collected in `$result->errors`. * * @param mixed|null $rootValue - * @param mixed[]|null $contextValue + * @param mixed|null $contextValue * @param mixed[]|ArrayAccess|null $variableValues * @param string|null $operationName * @@ -1121,8 +1118,8 @@ static function execute( * * Useful for async PHP platforms. * - * @param mixed[]|null $rootValue - * @param mixed[]|null $contextValue + * @param mixed|null $rootValue + * @param mixed|null $contextValue * @param mixed[]|null $variableValues * @param string|null $operationName * @@ -1228,16 +1225,13 @@ function setErrorsHandler(callable $handler) * If debug argument is passed, output of error formatter is enriched which debugging information * ("debugMessage", "trace" keys depending on flags). * - * $debug argument must be either bool (only adds "debugMessage" to result) or sum of flags from - * GraphQL\Error\Debug - * - * @param bool|int $debug + * $debug argument must sum of flags from @see \GraphQL\Error\DebugFlag * * @return mixed[] * * @api */ -function toArray($debug = false) +function toArray(int $debug = "GraphQL\Error\DebugFlag::NONE"): array ``` # GraphQL\Executor\Promise\PromiseAdapter Provides a means for integration of async PHP platforms ([related docs](data-fetching.md#async-php)) @@ -1486,7 +1480,7 @@ const ALL = 63; * * @api */ -static function setWarningHandler(callable $warningHandler = null) +static function setWarningHandler(callable $warningHandler = null): void ``` ```php @@ -1502,7 +1496,7 @@ static function setWarningHandler(callable $warningHandler = null) * * @api */ -static function suppress($suppress = true) +static function suppress($suppress = true): void ``` ```php @@ -1518,7 +1512,7 @@ static function suppress($suppress = true) * * @api */ -static function enable($enable = true) +static function enable($enable = true): void ``` # GraphQL\Error\ClientAware This interface is used for [default error formatting](error-handling.md). @@ -1552,11 +1546,12 @@ function isClientSafe() */ function getCategory() ``` -# GraphQL\Error\Debug +# GraphQL\Error\DebugFlag Collection of flags for [error debugging](error-handling.md#debugging-tools). **Class Constants:** ```php +const NONE = 0; const INCLUDE_DEBUG_MESSAGE = 1; const INCLUDE_TRACE = 2; const RETHROW_INTERNAL_EXCEPTIONS = 4; @@ -1589,11 +1584,9 @@ static function setInternalErrorMessage($msg) * This method only exposes exception message when exception implements ClientAware interface * (or when debug flags are passed). * - * For a list of available debug flags see GraphQL\Error\Debug constants. + * For a list of available debug flags @see \GraphQL\Error\DebugFlag constants. * - * @param Throwable $e - * @param bool|int $debug - * @param string $internalErrorMessage + * @param string $internalErrorMessage * * @return mixed[] * @@ -1601,7 +1594,11 @@ static function setInternalErrorMessage($msg) * * @api */ -static function createFromException($e, $debug = false, $internalErrorMessage = null) +static function createFromException( + Throwable $exception, + int $debug = "GraphQL\Error\DebugFlag::NONE", + $internalErrorMessage = null +): array ``` ```php @@ -1717,7 +1714,7 @@ function executeRequest($parsedBody = null) * @api */ function processPsrRequest( - Psr\Http\Message\ServerRequestInterface $request, + Psr\Http\Message\RequestInterface $request, Psr\Http\Message\ResponseInterface $response, Psr\Http\Message\StreamInterface $writableBodyStream ) @@ -1732,7 +1729,7 @@ function processPsrRequest( * * @api */ -function executePsrRequest(Psr\Http\Message\ServerRequestInterface $request) +function executePsrRequest(Psr\Http\Message\RequestInterface $request) ``` ```php @@ -1831,7 +1828,7 @@ function setErrorsHandler(callable $handler) /** * Set validation rules for this server. * - * @param ValidationRule[]|callable $validationRules + * @param ValidationRule[]|callable|null $validationRules * * @return self * @@ -1864,28 +1861,20 @@ function setPersistentQueryLoader(callable $persistentQueryLoader) ```php /** - * Set response debug flags. See GraphQL\Error\Debug class for a list of all available flags - * - * @param bool|int $set - * - * @return self + * Set response debug flags. @see \GraphQL\Error\DebugFlag class for a list of all available flags * * @api */ -function setDebug($set = true) +function setDebugFlag(int $debugFlag = "GraphQL\Error\DebugFlag::INCLUDE_DEBUG_MESSAGE"): self ``` ```php /** * Allow batching queries (disabled by default) * - * @param bool $enableBatching - * - * @return self - * * @api */ -function setQueryBatching($enableBatching) +function setQueryBatching(bool $enableBatching): self ``` ```php @@ -1950,7 +1939,7 @@ function parseRequestParams($method, array $bodyParams, array $queryParams) * Checks validity of OperationParams extracted from HTTP request and returns an array of errors * if params are invalid (or empty array when params are valid) * - * @return Error[] + * @return array * * @api */ @@ -2005,7 +1994,7 @@ function sendResponse($result, $exitWhenDone = false) * * @api */ -function parsePsrRequest(Psr\Http\Message\ServerRequestInterface $request) +function parsePsrRequest(Psr\Http\Message\RequestInterface $request) ``` ```php @@ -2059,6 +2048,12 @@ public $operation; * @var mixed[]|null */ public $variables; + +/** + * @api + * @var mixed[]|null + */ +public $extensions; ``` **Class Methods:** @@ -2067,13 +2062,10 @@ public $variables; * Creates an instance from given array * * @param mixed[] $params - * @param bool $readonly - * - * @return OperationParams * * @api */ -static function create(array $params, $readonly = false) +static function create(array $params, bool $readonly = false): GraphQL\Server\OperationParams ``` ```php @@ -2133,6 +2125,7 @@ static function build($source, callable $typeConfigDecorator = null, array $opti * * - commentDescriptions: * Provide true to use preceding comments as the description. + * This option is provided to ease adoption and will be removed in v16. * * @param bool[] $options * @@ -2178,7 +2171,7 @@ Various utilities dealing with AST * * @api */ -static function fromArray(array $node) +static function fromArray(array $node): GraphQL\Language\AST\Node ``` ```php @@ -2189,7 +2182,7 @@ static function fromArray(array $node) * * @api */ -static function toArray(GraphQL\Language\AST\Node $node) +static function toArray(GraphQL\Language\AST\Node $node): array ``` ```php @@ -2213,7 +2206,7 @@ static function toArray(GraphQL\Language\AST\Node $node) * * @param Type|mixed|null $value * - * @return ObjectValueNode|ListValueNode|BooleanValueNode|IntValueNode|FloatValueNode|EnumValueNode|StringValueNode|NullValueNode + * @return ObjectValueNode|ListValueNode|BooleanValueNode|IntValueNode|FloatValueNode|EnumValueNode|StringValueNode|NullValueNode|null * * @api */ @@ -2240,8 +2233,8 @@ static function astFromValue($value, GraphQL\Type\Definition\InputType $type) * | Enum Value | Mixed | * | Null Value | null | * - * @param ValueNode|null $valueNode - * @param mixed[]|null $variables + * @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null $valueNode + * @param mixed[]|null $variables * * @return mixed[]|stdClass|null * @@ -2249,7 +2242,11 @@ static function astFromValue($value, GraphQL\Type\Definition\InputType $type) * * @api */ -static function valueFromAST($valueNode, GraphQL\Type\Definition\InputType $type, array $variables = null) +static function valueFromAST( + GraphQL\Language\AST\ValueNode $valueNode, + GraphQL\Type\Definition\Type $type, + array $variables = null +) ``` ```php @@ -2302,7 +2299,7 @@ static function typeFromAST(GraphQL\Type\Schema $schema, $inputTypeNode) * * @param string $operationName * - * @return bool + * @return bool|string * * @api */ @@ -2314,23 +2311,22 @@ Given an instance of Schema, prints it in GraphQL type language. **Class Methods:** ```php /** - * Accepts options as a second argument: - * + * @param array $options + * Available options: * - commentDescriptions: * Provide true to use preceding comments as the description. - * - * @param bool[] $options + * This option is provided to ease adoption and will be removed in v16. * * @api */ -static function doPrint(GraphQL\Type\Schema $schema, array $options = []) +static function doPrint(GraphQL\Type\Schema $schema, array $options = []): string ``` ```php /** - * @param bool[] $options + * @param array $options * * @api */ -static function printIntrospectionSchema(GraphQL\Type\Schema $schema, array $options = []) +static function printIntrospectionSchema(GraphQL\Type\Schema $schema, array $options = []): string ``` diff --git a/docs/type-system/enum-types.md b/docs/type-system/enum-types.md index 4cebdec70..bfab3a549 100644 --- a/docs/type-system/enum-types.md +++ b/docs/type-system/enum-types.md @@ -158,7 +158,7 @@ $heroType = new ObjectType([ 'args' => [ 'episode' => Type::nonNull($enumType) ], - 'resolve' => function($_value, $args) { + 'resolve' => function($hero, $args) { return $args['episode'] === 5 ? true : false; } ] diff --git a/docs/type-system/input-types.md b/docs/type-system/input-types.md index 1b7ffc814..7138557bf 100644 --- a/docs/type-system/input-types.md +++ b/docs/type-system/input-types.md @@ -131,7 +131,7 @@ $queryType = new ObjectType([ 'type' => Type::listOf($storyType), 'args' => [ 'filters' => [ - 'type' => Type::nonNull($filters), + 'type' => $filters, 'defaultValue' => [ 'popular' => true ] diff --git a/docs/type-system/interfaces.md b/docs/type-system/interfaces.md index 5335d6d7b..100d510ef 100644 --- a/docs/type-system/interfaces.md +++ b/docs/type-system/interfaces.md @@ -134,3 +134,41 @@ concrete Object Type. If a **resolveType** option is omitted, graphql-php will loop through all interface implementors and use their **isTypeOf** callback to pick the first suitable one. This is obviously less efficient than single **resolveType** call. So it is recommended to define **resolveType** whenever possible. + +# Prevent invisible types +When object types that implement an interface are not directly referenced by a field, they cannot +be discovered during schema introspection. For example: + +```graphql +type Query { + animal: Animal +} + +interface Animal {...} + +type Cat implements Animal {...} +type Dog implements Animal {...} +``` + +In this example, `Cat` and `Dog` would be considered *invisible* types. Querying the `animal` field +would fail, since no possible implementing types for `Animal` can be found. + +There are two possible solutions: + +1. Add fields that reference the invisible types directly, e.g.: + + ```graphql + type Query { + dog: Dog + cat: Cat + } + ``` + +2. Pass the invisible types during schema construction, e.g.: + +```php +new GraphQLSchema([ + 'query' => ..., + 'types' => [$cat, $dog] +]); +``` diff --git a/docs/type-system/object-types.md b/docs/type-system/object-types.md index 9c1f2fbc4..0b950c021 100644 --- a/docs/type-system/object-types.md +++ b/docs/type-system/object-types.md @@ -80,7 +80,7 @@ Option | Type | Notes name | `string` | **Required.** Name of the field. When not set - inferred from **fields** array key (read about [shorthand field definition](#shorthand-field-definitions) below) type | `Type` | **Required.** An instance of internal or custom type. Note: type must be represented by a single instance within one schema (see also [Type Registry](index.md#type-registry)) args | `array` | An array of possible type arguments. Each entry is expected to be an array with keys: **name**, **type**, **description**, **defaultValue**. See [Field Arguments](#field-arguments) section below. -resolve | `callable` | **function($value, $args, $context, [ResolveInfo](../reference.md#graphqltypedefinitionresolveinfo) $info)**
Given the **$value** of this type, it is expected to return actual value of the current field. See section on [Data Fetching](../data-fetching.md) for details +resolve | `callable` | **function($objectValue, $args, $context, [ResolveInfo](../reference.md#graphqltypedefinitionresolveinfo) $info)**
Given the **$objectValue** of this type, it is expected to return actual value of the current field. See section on [Data Fetching](../data-fetching.md) for details complexity | `callable` | **function($childrenComplexity, $args)**
Used to restrict query complexity. The feature is disabled by default, read about [Security](../security.md#query-complexity-analysis) to use it. description | `string` | Plain-text description of this field for clients (e.g. used by [GraphiQL](https://github.com/graphql/graphiql) for auto-generated documentation) deprecationReason | `string` | Text describing why this field is deprecated. When not empty - field will not be returned by introspection queries (unless forced) diff --git a/docs/type-system/scalar-types.md b/docs/type-system/scalar-types.md index 302923838..074e8c69a 100644 --- a/docs/type-system/scalar-types.md +++ b/docs/type-system/scalar-types.md @@ -100,7 +100,7 @@ class EmailType extends ScalarType * @return string * @throws Error */ - public function parseLiteral($valueNode, array $variables = null) + public function parseLiteral(Node $valueNode, ?array $variables = null) { // Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL // error location in query: diff --git a/docs/type-system/schema.md b/docs/type-system/schema.md index 08ee12fce..e3ae6e052 100644 --- a/docs/type-system/schema.md +++ b/docs/type-system/schema.md @@ -62,7 +62,7 @@ $mutationType = new ObjectType([ 'episode' => $episodeEnum, 'review' => $reviewInputObject ], - 'resolve' => function($val, $args) { + 'resolve' => function($rootValue, $args) { // TODOC } ] diff --git a/examples/00-hello-world/graphql.php b/examples/00-hello-world/graphql.php index 30b1f9c0e..7f933ad16 100644 --- a/examples/00-hello-world/graphql.php +++ b/examples/00-hello-world/graphql.php @@ -19,8 +19,8 @@ 'args' => [ 'message' => ['type' => Type::string()], ], - 'resolve' => function ($root, $args) { - return $root['prefix'] . $args['message']; + 'resolve' => function ($rootValue, $args) { + return $rootValue['prefix'] . $args['message']; } ], ], @@ -35,7 +35,7 @@ 'x' => ['type' => Type::int()], 'y' => ['type' => Type::int()], ], - 'resolve' => function ($root, $args) { + 'resolve' => function ($calc, $args) { return $args['x'] + $args['y']; }, ], diff --git a/examples/01-blog/Blog/AppContext.php b/examples/01-blog/Blog/AppContext.php index f6c16cf8c..04918ae2c 100644 --- a/examples/01-blog/Blog/AppContext.php +++ b/examples/01-blog/Blog/AppContext.php @@ -4,10 +4,7 @@ use GraphQL\Examples\Blog\Data\User; /** - * Class AppContext * Instance available in all GraphQL resolvers as 3rd argument - * - * @package GraphQL\Examples\Blog */ class AppContext { diff --git a/examples/01-blog/Blog/Data/DataSource.php b/examples/01-blog/Blog/Data/DataSource.php index 09c1e0232..e8acd31fa 100644 --- a/examples/01-blog/Blog/Data/DataSource.php +++ b/examples/01-blog/Blog/Data/DataSource.php @@ -2,12 +2,8 @@ namespace GraphQL\Examples\Blog\Data; /** - * Class DataSource - * * This is just a simple in-memory data holder for the sake of example. * Data layer for real app may use Doctrine or query the database directly (e.g. in CQRS style) - * - * @package GraphQL\Examples\Blog */ class DataSource { diff --git a/examples/01-blog/Blog/Type/CommentType.php b/examples/01-blog/Blog/Type/CommentType.php index cbcad4dac..8a97a3a8f 100644 --- a/examples/01-blog/Blog/Type/CommentType.php +++ b/examples/01-blog/Blog/Type/CommentType.php @@ -35,12 +35,12 @@ public function __construct() Types::htmlField('body') ]; }, - 'resolveField' => function($value, $args, $context, ResolveInfo $info) { + 'resolveField' => function($comment, $args, $context, ResolveInfo $info) { $method = 'resolve' . ucfirst($info->fieldName); if (method_exists($this, $method)) { - return $this->{$method}($value, $args, $context, $info); + return $this->{$method}($comment, $args, $context, $info); } else { - return $value->{$info->fieldName}; + return $comment->{$info->fieldName}; } } ]; diff --git a/examples/01-blog/Blog/Type/QueryType.php b/examples/01-blog/Blog/Type/QueryType.php index ff8557783..b7d807568 100644 --- a/examples/01-blog/Blog/Type/QueryType.php +++ b/examples/01-blog/Blog/Type/QueryType.php @@ -57,8 +57,8 @@ public function __construct() ], 'hello' => Type::string() ], - 'resolveField' => function($val, $args, $context, ResolveInfo $info) { - return $this->{$info->fieldName}($val, $args, $context, $info); + 'resolveField' => function($rootValue, $args, $context, ResolveInfo $info) { + return $this->{$info->fieldName}($rootValue, $args, $context, $info); } ]; parent::__construct($config); diff --git a/examples/01-blog/Blog/Type/Scalar/EmailType.php b/examples/01-blog/Blog/Type/Scalar/EmailType.php index fd78ea796..ec304c168 100644 --- a/examples/01-blog/Blog/Type/Scalar/EmailType.php +++ b/examples/01-blog/Blog/Type/Scalar/EmailType.php @@ -6,15 +6,14 @@ use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Utils\Utils; -class EmailType +class EmailType extends CustomScalarType { - public static function create() + public function __construct(array $config = []) { - return new CustomScalarType([ - 'name' => 'Email', - 'serialize' => [__CLASS__, 'serialize'], - 'parseValue' => [__CLASS__, 'parseValue'], - 'parseLiteral' => [__CLASS__, 'parseLiteral'], + parent::__construct([ + 'serialize' => [__CLASS__, 's_serialize'], + 'parseValue' => [__CLASS__, 's_parseValue'], + 'parseLiteral' => [__CLASS__, 's_parseLiteral'], ]); } @@ -24,7 +23,7 @@ public static function create() * @param string $value * @return string */ - public static function serialize($value) + public static function s_serialize($value) { // Assuming internal representation of email is always correct: return $value; @@ -40,7 +39,7 @@ public static function serialize($value) * @param mixed $value * @return mixed */ - public static function parseValue($value) + public static function s_parseValue($value) { if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { throw new \UnexpectedValueException("Cannot represent value as email: " . Utils::printSafe($value)); @@ -55,7 +54,7 @@ public static function parseValue($value) * @return string * @throws Error */ - public static function parseLiteral($valueNode) + public static function s_parseLiteral($valueNode) { // Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL // error location in query: diff --git a/examples/01-blog/Blog/Type/Scalar/UrlType.php b/examples/01-blog/Blog/Type/Scalar/UrlType.php index 0590546f4..e9793f0ed 100644 --- a/examples/01-blog/Blog/Type/Scalar/UrlType.php +++ b/examples/01-blog/Blog/Type/Scalar/UrlType.php @@ -48,7 +48,7 @@ public function parseValue($value) * @return null|string * @throws Error */ - public function parseLiteral($valueNode, array $variables = null) + public function parseLiteral(Node $valueNode, ?array $variables = null) { // Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL // error location in query: diff --git a/examples/01-blog/Blog/Type/StoryType.php b/examples/01-blog/Blog/Type/StoryType.php index 32cea4e70..9a1b0416d 100644 --- a/examples/01-blog/Blog/Type/StoryType.php +++ b/examples/01-blog/Blog/Type/StoryType.php @@ -9,10 +9,6 @@ use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; -/** - * Class StoryType - * @package GraphQL\Examples\Social\Type - */ class StoryType extends ObjectType { const EDIT = 'EDIT'; @@ -75,12 +71,12 @@ public function __construct() 'interfaces' => [ Types::node() ], - 'resolveField' => function($value, $args, $context, ResolveInfo $info) { + 'resolveField' => function($story, $args, $context, ResolveInfo $info) { $method = 'resolve' . ucfirst($info->fieldName); if (method_exists($this, $method)) { - return $this->{$method}($value, $args, $context, $info); + return $this->{$method}($story, $args, $context, $info); } else { - return $value->{$info->fieldName}; + return $story->{$info->fieldName}; } } ]; @@ -124,4 +120,16 @@ public function resolveComments(Story $story, $args) $args += ['after' => null]; return DataSource::findComments($story->id, $args['limit'], $args['after']); } + + public function resolveMentions(Story $story, $args, AppContext $context){ + return DataSource::findStoryMentions($story->id); + } + + public function resolveLikedBy(Story $story, $args, AppContext $context){ + return DataSource::findLikes($story->id,10); + } + + public function resolveLikes(Story $story, $args, AppContext $context){ + return DataSource::findLikes($story->id,10); + } } diff --git a/examples/01-blog/Blog/Type/UserType.php b/examples/01-blog/Blog/Type/UserType.php index 9960b6265..b185f74a5 100644 --- a/examples/01-blog/Blog/Type/UserType.php +++ b/examples/01-blog/Blog/Type/UserType.php @@ -44,12 +44,12 @@ public function __construct() 'interfaces' => [ Types::node() ], - 'resolveField' => function($value, $args, $context, ResolveInfo $info) { + 'resolveField' => function($user, $args, $context, ResolveInfo $info) { $method = 'resolve' . ucfirst($info->fieldName); if (method_exists($this, $method)) { - return $this->{$method}($value, $args, $context, $info); + return $this->{$method}($user, $args, $context, $info); } else { - return $value->{$info->fieldName}; + return $user->{$info->fieldName}; } } ]; diff --git a/examples/01-blog/Blog/Types.php b/examples/01-blog/Blog/Types.php index 7ffc65734..9dddaec3b 100644 --- a/examples/01-blog/Blog/Types.php +++ b/examples/01-blog/Blog/Types.php @@ -1,13 +1,13 @@ Types::query() + 'query' => new QueryType(), + 'typeLoader' => function($name) { + return Types::byTypeName($name, true); + } ]); $result = GraphQL::executeQuery( diff --git a/examples/02-shorthand/rootvalue.php b/examples/02-shorthand/rootvalue.php index 97e0a82c1..c16d62bca 100644 --- a/examples/02-shorthand/rootvalue.php +++ b/examples/02-shorthand/rootvalue.php @@ -1,12 +1,12 @@ function($root, $args, $context) { + 'sum' => function($rootValue, $args, $context) { $sum = new Addition(); - return $sum->resolve($root, $args, $context); + return $sum->resolve($rootValue, $args, $context); }, - 'echo' => function($root, $args, $context) { + 'echo' => function($rootValue, $args, $context) { $echo = new Echoer(); - return $echo->resolve($root, $args, $context); + return $echo->resolve($rootValue, $args, $context); }, 'prefix' => 'You said: ', ]; diff --git a/examples/03-server/graphql.php b/examples/03-server/graphql.php index 0c0119596..c0240da57 100644 --- a/examples/03-server/graphql.php +++ b/examples/03-server/graphql.php @@ -19,8 +19,8 @@ 'args' => [ 'message' => ['type' => Type::string()], ], - 'resolve' => function ($root, $args) { - return $root['prefix'] . $args['message']; + 'resolve' => function ($rootValue, $args) { + return $rootValue['prefix'] . $args['message']; } ], ], @@ -35,7 +35,7 @@ 'x' => ['type' => Type::int()], 'y' => ['type' => Type::int()], ], - 'resolve' => function ($root, $args) { + 'resolve' => function ($calc, $args) { return $args['x'] + $args['y']; }, ], diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 000000000..ed143602d --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,657 @@ +parameters: + ignoreErrors: + - + message: "#^Variable property access on object\\.$#" + count: 2 + path: src/Executor/Executor.php + + - + message: "#^Variable property access on object\\.$#" + count: 2 + path: src/Experimental/Executor/CoroutineExecutor.php + + - + message: "#^Variable property access on mixed\\.$#" + count: 1 + path: src/Experimental/Executor/CoroutineExecutor.php + + - + message: "#^Variable property access on GraphQL\\\\Language\\\\AST\\\\Node\\.$#" + count: 1 + path: src/Language/AST/Node.php + + - + message: "#^Only booleans are allowed in a negated boolean, array\\ given\\.$#" + count: 1 + path: src/Language/Visitor.php + + - + message: "#^Variable property access on GraphQL\\\\Language\\\\AST\\\\Node\\|null\\.$#" + count: 1 + path: src/Language/Visitor.php + + - + message: "#^Variable property access on GraphQL\\\\Language\\\\AST\\\\Node\\.$#" + count: 1 + path: src/Language/Visitor.php + + - + message: "#^Only booleans are allowed in a negated boolean, \\(callable\\)\\|null given\\.$#" + count: 1 + path: src/Language/Visitor.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, \\(callable\\)\\|null given\\.$#" + count: 2 + path: src/Server/Helper.php + + - + message: "#^Only booleans are allowed in an if condition, int given\\.$#" + count: 1 + path: src/Server/Helper.php + + - + message: "#^Only booleans are allowed in a negated boolean, string given\\.$#" + count: 2 + path: src/Server/Helper.php + + - + message: "#^Only booleans are allowed in &&, string given on the left side\\.$#" + count: 1 + path: src/Server/Helper.php + + - + message: "#^Only booleans are allowed in &&, string given on the right side\\.$#" + count: 1 + path: src/Server/Helper.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, string given\\.$#" + count: 1 + path: src/Server/Helper.php + + - + message: "#^Only booleans are allowed in an if condition, \\(callable\\)\\|null given\\.$#" + count: 1 + path: src/Server/Helper.php + + - + message: "#^Only booleans are allowed in \\|\\|, \\(callable\\)\\|null given on the left side\\.$#" + count: 1 + path: src/Server/Helper.php + + - + message: "#^Only booleans are allowed in a negated boolean, ArrayObject\\ given\\.$#" + count: 1 + path: src/Type/Definition/EnumType.php + + - + message: "#^Variable property access on \\$this\\(GraphQL\\\\Type\\\\Definition\\\\FieldDefinition\\)\\.$#" + count: 3 + path: src/Type/Definition/FieldDefinition.php + + - + message: "#^Variable property access on \\$this\\(GraphQL\\\\Type\\\\Definition\\\\InputObjectField\\)\\.$#" + count: 4 + path: src/Type/Definition/InputObjectField.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Language\\\\AST\\\\SelectionSetNode\\|null given\\.$#" + count: 1 + path: src/Type/Definition/QueryPlan.php + + - + message: "#^Only booleans are allowed in an if condition, GraphQL\\\\Language\\\\AST\\\\SelectionSetNode\\|null given\\.$#" + count: 1 + path: src/Type/Definition/QueryPlan.php + + - + message: "#^Only booleans are allowed in an if condition, string given\\.$#" + count: 1 + path: src/Type/Definition/Type.php + + - + message: "#^Only booleans are allowed in a negated boolean, string\\|null given\\.$#" + count: 2 + path: src/Type/Introspection.php + + - + message: "#^Anonymous function should have native return typehint \"array\"\\.$#" + count: 1 + path: src/Type/Introspection.php + + - + message: "#^Only booleans are allowed in a negated boolean, array\\\\|\\(callable\\) given\\.$#" + count: 1 + path: src/Type/Schema.php + + - + message: "#^Only booleans are allowed in an if condition, \\(callable\\)\\|null given\\.$#" + count: 1 + path: src/Type/Schema.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Type\\\\Definition\\\\Type\\|null given\\.$#" + count: 1 + path: src/Type/Schema.php + + - + message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" + count: 1 + path: src/Type/Schema.php + + - + message: "#^Only booleans are allowed in a negated boolean, \\(callable\\)\\|null given\\.$#" + count: 1 + path: src/Type/Schema.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Type\\\\Definition\\\\ObjectType\\|null given\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in &&, GraphQL\\\\Type\\\\Definition\\\\ObjectType\\|null given on the left side\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in &&, array\\\\|GraphQL\\\\Language\\\\AST\\\\Node\\|GraphQL\\\\Language\\\\AST\\\\TypeDefinitionNode\\|GraphQL\\\\Language\\\\AST\\\\TypeNode\\|null given on the left side\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, GraphQL\\\\Language\\\\AST\\\\OperationTypeDefinitionNode\\|null given\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, GraphQL\\\\Type\\\\Definition\\\\Type given\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Error\\\\Error\\|null given\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, GraphQL\\\\Language\\\\AST\\\\InputValueDefinitionNode given\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, GraphQL\\\\Language\\\\AST\\\\DirectiveDefinitionNode\\|null given\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in a negated boolean, array\\ given\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in &&, array\\ given on the left side\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, GraphQL\\\\Language\\\\AST\\\\DirectiveDefinitionNode\\|GraphQL\\\\Language\\\\AST\\\\EnumTypeDefinitionNode\\|GraphQL\\\\Language\\\\AST\\\\InputObjectTypeDefinitionNode\\|GraphQL\\\\Language\\\\AST\\\\InterfaceTypeDefinitionNode\\|GraphQL\\\\Language\\\\AST\\\\ObjectTypeDefinitionNode\\|GraphQL\\\\Language\\\\AST\\\\SchemaDefinitionNode\\|GraphQL\\\\Language\\\\AST\\\\UnionTypeDefinitionNode\\|null given\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, array\\ given\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Language\\\\AST\\\\InterfaceTypeDefinitionNode\\|GraphQL\\\\Language\\\\AST\\\\InterfaceTypeExtensionNode\\|GraphQL\\\\Language\\\\AST\\\\ObjectTypeDefinitionNode\\|GraphQL\\\\Language\\\\AST\\\\ObjectTypeExtensionNode given\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, GraphQL\\\\Language\\\\AST\\\\FieldDefinitionNode\\|null given\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in &&, GraphQL\\\\Language\\\\AST\\\\FieldDefinitionNode\\|null given on the left side\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in &&, GraphQL\\\\Language\\\\AST\\\\NodeList\\ given on the right side\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, GraphQL\\\\Language\\\\AST\\\\InputValueDefinitionNode\\|null given\\.$#" + count: 2 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Type\\\\Definition\\\\FieldDefinition\\|null given\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Type\\\\Definition\\\\FieldArgument\\|null given\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in \\|\\|, GraphQL\\\\Type\\\\Definition\\\\FieldArgument\\|null given on the left side\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in a negated boolean, array\\ given\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in a negated boolean, array\\ given\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in &&, array\\ given on the left side\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Only booleans are allowed in a negated boolean, array\\ given\\.$#" + count: 1 + path: src/Type/SchemaValidationContext.php + + - + message: "#^Variable property access on mixed\\.$#" + count: 2 + path: src/Utils/AST.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Language\\\\AST\\\\BooleanValueNode\\|GraphQL\\\\Language\\\\AST\\\\EnumValueNode\\|GraphQL\\\\Language\\\\AST\\\\FloatValueNode\\|GraphQL\\\\Language\\\\AST\\\\IntValueNode\\|GraphQL\\\\Language\\\\AST\\\\ListValueNode\\|GraphQL\\\\Language\\\\AST\\\\NullValueNode\\|GraphQL\\\\Language\\\\AST\\\\ObjectValueNode\\|GraphQL\\\\Language\\\\AST\\\\StringValueNode\\|null given\\.$#" + count: 2 + path: src/Utils/AST.php + + - + message: "#^Only booleans are allowed in a negated boolean, array\\|null given\\.$#" + count: 1 + path: src/Utils/AST.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Type\\\\Definition\\\\EnumValueDefinition\\|null given\\.$#" + count: 1 + path: src/Utils/AST.php + + - + message: "#^Only booleans are allowed in &&, array\\|null given on the left side\\.$#" + count: 1 + path: src/Utils/AST.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, GraphQL\\\\Type\\\\Definition\\\\Type\\|null given\\.$#" + count: 2 + path: src/Utils/AST.php + + - + message: "#^Only booleans are allowed in an if condition, GraphQL\\\\Language\\\\AST\\\\NodeList\\ given\\.$#" + count: 1 + path: src/Utils/AST.php + + - + message: "#^Only booleans are allowed in a negated boolean, string\\|null given\\.$#" + count: 1 + path: src/Utils/AST.php + + - + message: "#^Only booleans are allowed in an if condition, callable given\\.$#" + count: 1 + path: src/Utils/ASTDefinitionBuilder.php + + - + message: "#^Only booleans are allowed in &&, array\\\\|null given on the left side\\.$#" + count: 1 + path: src/Utils/PairSet.php + + - + message: "#^Only booleans are allowed in an if condition, GraphQL\\\\Type\\\\Definition\\\\Type\\|null given\\.$#" + count: 1 + path: src/Utils/SchemaExtender.php + + - + message: "#^Only booleans are allowed in an if condition, GraphQL\\\\Language\\\\AST\\\\SchemaDefinitionNode\\|null given\\.$#" + count: 1 + path: src/Utils/SchemaExtender.php + + - + message: "#^Only booleans are allowed in a negated boolean, array\\\\|null given\\.$#" + count: 1 + path: src/Utils/TypeInfo.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Type\\\\Definition\\\\Type\\|null given\\.$#" + count: 1 + path: src/Utils/TypeInfo.php + + - + message: "#^Only booleans are allowed in an if condition, \\(GraphQL\\\\Type\\\\Definition\\\\CompositeType&GraphQL\\\\Type\\\\Definition\\\\Type\\)\\|null given\\.$#" + count: 1 + path: src/Utils/TypeInfo.php + + - + message: "#^Only booleans are allowed in an if condition, GraphQL\\\\Type\\\\Definition\\\\FieldDefinition\\|null given\\.$#" + count: 1 + path: src/Utils/TypeInfo.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, GraphQL\\\\Language\\\\AST\\\\NamedTypeNode given\\.$#" + count: 1 + path: src/Utils/TypeInfo.php + + - + message: "#^Only booleans are allowed in an if condition, GraphQL\\\\Type\\\\Definition\\\\Directive\\|GraphQL\\\\Type\\\\Definition\\\\FieldDefinition\\|null given\\.$#" + count: 1 + path: src/Utils/TypeInfo.php + + - + message: "#^Only booleans are allowed in &&, GraphQL\\\\Type\\\\Definition\\\\FieldArgument\\|null given on the left side\\.$#" + count: 1 + path: src/Utils/TypeInfo.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, GraphQL\\\\Type\\\\Definition\\\\InputObjectField\\|null given\\.$#" + count: 1 + path: src/Utils/TypeInfo.php + + - + message: "#^Only booleans are allowed in &&, GraphQL\\\\Type\\\\Definition\\\\InputObjectField\\|null given on the left side\\.$#" + count: 1 + path: src/Utils/TypeInfo.php + + - + message: "#^Variable property access on object\\.$#" + count: 1 + path: src/Utils/Utils.php + + - + message: "#^Only booleans are allowed in a negated boolean, string given\\.$#" + count: 1 + path: src/Utils/Utils.php + + - + message: "#^Only booleans are allowed in an if condition, GraphQL\\\\Error\\\\Error\\|null given\\.$#" + count: 1 + path: src/Utils/Utils.php + + - + message: "#^Only booleans are allowed in an if condition, GraphQL\\\\Type\\\\Definition\\\\EnumValueDefinition\\|null given\\.$#" + count: 1 + path: src/Utils/Value.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, array\\ given\\.$#" + count: 2 + path: src/Utils/Value.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, array\\ given\\.$#" + count: 2 + path: src/Utils/Value.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, string given\\.$#" + count: 2 + path: src/Utils/Value.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, string\\|null given\\.$#" + count: 1 + path: src/Utils/Value.php + + - + message: "#^Only booleans are allowed in a negated boolean, \\(GraphQL\\\\Type\\\\Definition\\\\CompositeType&GraphQL\\\\Type\\\\Definition\\\\Type\\)\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/FieldsOnCorrectType.php + + - + message: "#^Only booleans are allowed in an if condition, GraphQL\\\\Type\\\\Definition\\\\FieldDefinition given\\.$#" + count: 1 + path: src/Validator/Rules/FieldsOnCorrectType.php + + - + message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" + count: 1 + path: src/Validator/Rules/FieldsOnCorrectType.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Language\\\\AST\\\\NamedTypeNode given\\.$#" + count: 1 + path: src/Validator/Rules/FragmentsOnCompositeTypes.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Type\\\\Definition\\\\Type\\|null given\\.$#" + count: 2 + path: src/Validator/Rules/FragmentsOnCompositeTypes.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, GraphQL\\\\Type\\\\Schema\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/KnownDirectives.php + + - + message: "#^Only booleans are allowed in a negated boolean, array\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/KnownDirectives.php + + - + message: "#^Only booleans are allowed in a negated boolean, string given\\.$#" + count: 1 + path: src/Validator/Rules/KnownDirectives.php + + - + message: "#^Only booleans are allowed in an if condition, GraphQL\\\\Language\\\\AST\\\\FragmentDefinitionNode\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/KnownFragmentNames.php + + - + message: "#^Only booleans are allowed in an if condition, GraphQL\\\\Language\\\\AST\\\\FragmentDefinitionNode\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/NoFragmentCycles.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, GraphQL\\\\Language\\\\AST\\\\NameNode\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/OverlappingFieldsCanBeMerged.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, GraphQL\\\\Language\\\\AST\\\\NamedTypeNode given\\.$#" + count: 1 + path: src/Validator/Rules/OverlappingFieldsCanBeMerged.php + + - + message: "#^Only booleans are allowed in a negated boolean, array\\|null given\\.$#" + count: 2 + path: src/Validator/Rules/OverlappingFieldsCanBeMerged.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Language\\\\AST\\\\ArgumentNode\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/OverlappingFieldsCanBeMerged.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Language\\\\AST\\\\Node given\\.$#" + count: 2 + path: src/Validator/Rules/OverlappingFieldsCanBeMerged.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Language\\\\AST\\\\FragmentDefinitionNode\\|null given\\.$#" + count: 3 + path: src/Validator/Rules/OverlappingFieldsCanBeMerged.php + + - + message: "#^Only booleans are allowed in a negated boolean, \\(GraphQL\\\\Type\\\\Definition\\\\CompositeType&GraphQL\\\\Type\\\\Definition\\\\Type\\)\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/PossibleFragmentSpreads.php + + - + message: "#^Only booleans are allowed in an if condition, GraphQL\\\\Language\\\\AST\\\\FragmentDefinitionNode\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/PossibleFragmentSpreads.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Type\\\\Definition\\\\FieldDefinition given\\.$#" + count: 1 + path: src/Validator/Rules/ProvidedRequiredArguments.php + + - + message: "#^Only booleans are allowed in \\|\\|, GraphQL\\\\Language\\\\AST\\\\ArgumentNode\\|null given on the left side\\.$#" + count: 1 + path: src/Validator/Rules/ProvidedRequiredArguments.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, GraphQL\\\\Type\\\\Schema\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php + + - + message: "#^Only booleans are allowed in a negated boolean, array\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php + + - + message: "#^Only booleans are allowed in &&, GraphQL\\\\Type\\\\Definition\\\\Type\\|null given on the left side\\.$#" + count: 1 + path: src/Validator/Rules/QuerySecurityRule.php + + - + message: "#^Only booleans are allowed in an if condition, GraphQL\\\\Language\\\\AST\\\\FragmentDefinitionNode\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/QuerySecurityRule.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, GraphQL\\\\Language\\\\AST\\\\NameNode\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/QuerySecurityRule.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Type\\\\Definition\\\\OutputType\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/ScalarLeafs.php + + - + message: "#^Only booleans are allowed in an if condition, GraphQL\\\\Language\\\\AST\\\\SelectionSetNode\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/ScalarLeafs.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Language\\\\AST\\\\SelectionSetNode\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/ScalarLeafs.php + + - + message: "#^Only booleans are allowed in \\|\\|, GraphQL\\\\Type\\\\Definition\\\\EnumType\\|GraphQL\\\\Type\\\\Definition\\\\InputObjectType\\|GraphQL\\\\Type\\\\Definition\\\\ListOfType\\|GraphQL\\\\Type\\\\Definition\\\\NonNull\\|GraphQL\\\\Type\\\\Definition\\\\ScalarType given on the left side\\.$#" + count: 1 + path: src/Validator/Rules/ValuesOfCorrectType.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Type\\\\Definition\\\\EnumValueDefinition\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/ValuesOfCorrectType.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Type\\\\Definition\\\\EnumType\\|GraphQL\\\\Type\\\\Definition\\\\InputObjectType\\|GraphQL\\\\Type\\\\Definition\\\\ListOfType\\|GraphQL\\\\Type\\\\Definition\\\\NonNull\\|GraphQL\\\\Type\\\\Definition\\\\ScalarType given\\.$#" + count: 1 + path: src/Validator/Rules/ValuesOfCorrectType.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, array\\ given\\.$#" + count: 1 + path: src/Validator/Rules/ValuesOfCorrectType.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Type\\\\Definition\\\\Type\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/VariablesAreInputTypes.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Type\\\\Definition\\\\Type\\|null given\\.$#" + count: 1 + path: src/Validator/Rules/VariablesInAllowedPosition.php + + - + message: "#^Only booleans are allowed in &&, GraphQL\\\\Language\\\\AST\\\\ValueNode\\|null given on the left side\\.$#" + count: 1 + path: src/Validator/Rules/VariablesInAllowedPosition.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Language\\\\AST\\\\FragmentDefinitionNode\\|null given\\.$#" + count: 1 + path: src/Validator/ValidationContext.php + + - + message: "#^Only booleans are allowed in an if condition, GraphQL\\\\Language\\\\AST\\\\SelectionSetNode\\|null given\\.$#" + count: 1 + path: src/Validator/ValidationContext.php + + - + message: "#^Only booleans are allowed in a negated boolean, array\\ given\\.$#" + count: 1 + path: src/Validator/ValidationContext.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Type\\\\Schema given\\.$#" + count: 1 + path: tests/Executor/DirectivesTest.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Type\\\\Definition\\\\InterfaceType given\\.$#" + count: 1 + path: tests/Executor/LazyInterfaceTest.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Type\\\\Definition\\\\ObjectType given\\.$#" + count: 1 + path: tests/Executor/LazyInterfaceTest.php + + - + message: "#^Only booleans are allowed in a negated boolean, GraphQL\\\\Type\\\\Schema given\\.$#" + count: 1 + path: tests/Executor/ValuesTest.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, \\(GraphQL\\\\Type\\\\Definition\\\\CompositeType&GraphQL\\\\Type\\\\Definition\\\\Type\\)\\|null given\\.$#" + count: 4 + path: tests/Language/VisitorTest.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, \\(GraphQL\\\\Type\\\\Definition\\\\OutputType&GraphQL\\\\Type\\\\Definition\\\\Type\\)\\|null given\\.$#" + count: 4 + path: tests/Language/VisitorTest.php + + - + message: "#^Only booleans are allowed in a ternary operator condition, GraphQL\\\\Type\\\\Definition\\\\EnumType\\|GraphQL\\\\Type\\\\Definition\\\\InputObjectType\\|GraphQL\\\\Type\\\\Definition\\\\ListOfType\\|GraphQL\\\\Type\\\\Definition\\\\NonNull\\|GraphQL\\\\Type\\\\Definition\\\\ScalarType\\|null given\\.$#" + count: 4 + path: tests/Language/VisitorTest.php + + - + message: "#^Access to an undefined property GraphQL\\\\Type\\\\Definition\\\\FieldDefinition\\:\\:\\$nonExistentProp\\.$#" + count: 2 + path: tests/Type/DefinitionTest.php + + - + message: "#^Access to an undefined property GraphQL\\\\Type\\\\Definition\\\\InputObjectField\\:\\:\\$nonExistentProp\\.$#" + count: 1 + path: tests/Type/DefinitionTest.php + + - + message: "#^Variable property access on \\$this\\(GraphQL\\\\Tests\\\\Type\\\\TypeLoaderTest\\)\\.$#" + count: 1 + path: tests/Type/TypeLoaderTest.php + + - + message: "#^Only booleans are allowed in a negated boolean, stdClass given\\.$#" + count: 1 + path: tests/Utils/AstFromValueTest.php + diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 02a7a9a80..2d8ea2096 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,17 +1,47 @@ parameters: - level: 1 + level: 3 + + inferPrivatePropertyTypeFromConstructor: true paths: - %currentWorkingDirectory%/src - %currentWorkingDirectory%/tests + excludes_analyse: + # Ported from dms/phpunit-arraysubset-asserts + - tests/PHPUnit/ArraySubsetAsserts.php + - tests/PHPUnit/Constraint/ArraySubset.php + ignoreErrors: - - "~Construct empty\\(\\) is not allowed\\. Use more strict comparison~" - - "~(Method|Property) .+::.+(\\(\\))? (has parameter \\$\\w+ with no|has no return|has no) typehint specified~" - - "~Variable property access on .+~" - - "~Variable method call on static\\(GraphQL\\\\Server\\\\ServerConfig\\)~" # TODO get rid of + # Since this is a library that is supposed to be flexible, we don't + # want to lock down every possible extension point. + - "~Unsafe usage of new static\\(\\)~" + + # This class uses magic methods to reduce a whole lot of boilerplate required to + # allow partial parsing of language fragments. + - "~Variable method call on GraphQL\\\\Language\\\\Parser\\.~" + + # Those come from graphql-php\tests\Language\VisitorTest.php + - "~Access to an undefined property GraphQL\\\\Language\\\\AST\\\\.+::\\$didEnter~" + - "~Access to an undefined property GraphQL\\\\Language\\\\AST\\\\.+::\\$didLeave~" + - "~Access to an undefined property GraphQL\\\\Language\\\\AST\\\\Node::\\$value~" + + # TODO convert to less magical code + - "~Variable method call on static\\(GraphQL\\\\Server\\\\ServerConfig\\)~" includes: - - vendor/phpstan/phpstan-phpunit/extension.neon - - vendor/phpstan/phpstan-phpunit/rules.neon - - vendor/phpstan/phpstan-strict-rules/rules.neon + - phpstan-baseline.neon + +services: + - + class: GraphQL\Tests\PhpStan\Type\Definition\Type\IsInputTypeStaticMethodTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension + - + class: GraphQL\Tests\PhpStan\Type\Definition\Type\IsOutputTypeStaticMethodTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension + - + class: GraphQL\Tests\PhpStan\Type\Definition\Type\IsCompositeTypeStaticMethodTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension diff --git a/src/Deferred.php b/src/Deferred.php index 6b7bd7824..ff79ea6bd 100644 --- a/src/Deferred.php +++ b/src/Deferred.php @@ -4,62 +4,23 @@ namespace GraphQL; -use Exception; use GraphQL\Executor\Promise\Adapter\SyncPromise; -use SplQueue; -use Throwable; -class Deferred +class Deferred extends SyncPromise { - /** @var SplQueue|null */ - private static $queue; - - /** @var callable */ - private $callback; - - /** @var SyncPromise */ - public $promise; - - public function __construct(callable $callback) - { - $this->callback = $callback; - $this->promise = new SyncPromise(); - self::getQueue()->enqueue($this); - } - - public static function getQueue() : SplQueue - { - if (self::$queue === null) { - self::$queue = new SplQueue(); - } - - return self::$queue; - } - - public static function runQueue() : void - { - $queue = self::getQueue(); - while (! $queue->isEmpty()) { - /** @var self $dequeuedNodeValue */ - $dequeuedNodeValue = $queue->dequeue(); - $dequeuedNodeValue->run(); - } - } - - public function then($onFulfilled = null, $onRejected = null) + /** + * @param callable() : mixed $executor + */ + public static function create(callable $executor) : self { - return $this->promise->then($onFulfilled, $onRejected); + return new self($executor); } - public function run() : void + /** + * @param callable() : mixed $executor + */ + public function __construct(callable $executor) { - try { - $cb = $this->callback; - $this->promise->resolve($cb()); - } catch (Exception $e) { - $this->promise->reject($e); - } catch (Throwable $e) { - $this->promise->reject($e); - } + parent::__construct($executor); } } diff --git a/src/Error/Debug.php b/src/Error/Debug.php deleted file mode 100644 index a48b81f43..000000000 --- a/src/Error/Debug.php +++ /dev/null @@ -1,16 +0,0 @@ -source = $source; $this->positions = $positions; $this->path = $path; - $this->extensions = $extensions ?: ( - $previous && $previous instanceof self + $this->extensions = count($extensions) > 0 ? $extensions : ( + $previous instanceof self ? $previous->extensions : [] ); if ($previous instanceof ClientAware) { $this->isClientSafe = $previous->isClientSafe(); - $this->category = $previous->getCategory() ?: self::CATEGORY_INTERNAL; - } elseif ($previous) { + $cat = $previous->getCategory(); + $this->category = $cat === '' || $cat === null ? self::CATEGORY_INTERNAL: $cat; + } elseif ($previous !== null) { $this->isClientSafe = false; $this->category = self::CATEGORY_INTERNAL; } else { @@ -146,25 +145,27 @@ public function __construct( public static function createLocatedError($error, $nodes = null, $path = null) { if ($error instanceof self) { - if ($error->path && $error->nodes) { + if ($error->path !== null && $error->nodes !== null && count($error->nodes) !== 0) { return $error; } - $nodes = $nodes ?: $error->nodes; - $path = $path ?: $error->path; + $nodes = $nodes ?? $error->nodes; + $path = $path ?? $error->path; } - $source = $positions = $originalError = null; - $extensions = []; + $source = null; + $originalError = null; + $positions = []; + $extensions = []; if ($error instanceof self) { $message = $error->getMessage(); $originalError = $error; - $nodes = $error->nodes ?: $nodes; + $nodes = $error->nodes ?? $nodes; $source = $error->source; $positions = $error->positions; $extensions = $error->extensions; - } elseif ($error instanceof Exception || $error instanceof Throwable) { + } elseif ($error instanceof Throwable) { $message = $error->getMessage(); $originalError = $error; } else { @@ -172,7 +173,7 @@ public static function createLocatedError($error, $nodes = null, $path = null) } return new static( - $message ?: 'An unknown error occurred.', + $message === '' || $message === null ? 'An unknown error occurred.' : $message, $nodes, $source, $positions, @@ -206,13 +207,10 @@ public function getCategory() return $this->category; } - /** - * @return Source|null - */ - public function getSource() + public function getSource() : ?Source { if ($this->source === null) { - if (! empty($this->nodes[0]) && ! empty($this->nodes[0]->loc)) { + if (isset($this->nodes[0]) && $this->nodes[0]->loc !== null) { $this->source = $this->nodes[0]->loc->source; } } @@ -223,11 +221,11 @@ public function getSource() /** * @return int[] */ - public function getPositions() + public function getPositions() : array { - if ($this->positions === null && ! empty($this->nodes)) { + if (count($this->positions) === 0 && count($this->nodes ?? []) > 0) { $positions = array_map( - static function ($node) { + static function ($node) : ?int { return isset($node->loc) ? $node->loc->start : null; }, $this->nodes @@ -235,7 +233,7 @@ static function ($node) { $positions = array_filter( $positions, - static function ($p) { + static function ($p) : bool { return $p !== null; } ); @@ -261,27 +259,29 @@ static function ($p) { * * @api */ - public function getLocations() + public function getLocations() : array { - if ($this->locations === null) { + if (! isset($this->locations)) { $positions = $this->getPositions(); $source = $this->getSource(); $nodes = $this->nodes; - if ($positions && $source) { + if ($source !== null && count($positions) !== 0) { $this->locations = array_map( - static function ($pos) use ($source) { + static function ($pos) use ($source) : SourceLocation { return $source->getLocation($pos); }, $positions ); - } elseif ($nodes) { + } elseif ($nodes !== null && count($nodes) !== 0) { $locations = array_filter( array_map( - static function ($node) { - if ($node->loc && $node->loc->source) { + static function ($node) : ?SourceLocation { + if (isset($node->loc->source)) { return $node->loc->source->getLocation($node->loc->start); } + + return null; }, $nodes ) @@ -330,6 +330,8 @@ public function getExtensions() * @deprecated Use FormattedError::createFromException() instead * * @return mixed[] + * + * @codeCoverageIgnore */ public function toSerializableArray() { @@ -339,18 +341,18 @@ public function toSerializableArray() $locations = Utils::map( $this->getLocations(), - static function (SourceLocation $loc) { + static function (SourceLocation $loc) : array { return $loc->toSerializableArray(); } ); - if (! empty($locations)) { + if (count($locations) > 0) { $arr['locations'] = $locations; } - if (! empty($this->path)) { + if (count($this->path ?? []) > 0) { $arr['path'] = $this->path; } - if (! empty($this->extensions)) { + if (count($this->extensions ?? []) > 0) { $arr['extensions'] = $this->extensions; } diff --git a/src/Error/FormattedError.php b/src/Error/FormattedError.php index 799ee7492..4748b69fe 100644 --- a/src/Error/FormattedError.php +++ b/src/Error/FormattedError.php @@ -6,7 +6,6 @@ use Countable; use ErrorException; -use Exception; use GraphQL\Language\AST\Node; use GraphQL\Language\Source; use GraphQL\Language\SourceLocation; @@ -67,10 +66,10 @@ public static function setInternalErrorMessage($msg) public static function printError(Error $error) { $printedLocations = []; - if ($error->nodes) { + if (count($error->nodes ?? []) !== 0) { /** @var Node $node */ foreach ($error->nodes as $node) { - if (! $node->loc) { + if ($node->loc === null) { continue; } @@ -83,14 +82,14 @@ public static function printError(Error $error) $node->loc->source->getLocation($node->loc->start) ); } - } elseif ($error->getSource() && $error->getLocations()) { + } elseif ($error->getSource() !== null && count($error->getLocations()) !== 0) { $source = $error->getSource(); - foreach ($error->getLocations() as $location) { + foreach (($error->getLocations() ?? []) as $location) { $printedLocations[] = self::highlightSourceAtLocation($source, $location); } } - return ! $printedLocations + return count($printedLocations) === 0 ? $error->getMessage() : implode("\n\n", array_merge([$error->getMessage()], $printedLocations)) . "\n"; } @@ -162,11 +161,9 @@ private static function lpad($len, $str) * This method only exposes exception message when exception implements ClientAware interface * (or when debug flags are passed). * - * For a list of available debug flags see GraphQL\Error\Debug constants. + * For a list of available debug flags @see \GraphQL\Error\DebugFlag constants. * - * @param Throwable $e - * @param bool|int $debug - * @param string $internalErrorMessage + * @param string $internalErrorMessage * * @return mixed[] * @@ -174,21 +171,15 @@ private static function lpad($len, $str) * * @api */ - public static function createFromException($e, $debug = false, $internalErrorMessage = null) + public static function createFromException(Throwable $exception, int $debug = DebugFlag::NONE, $internalErrorMessage = null) : array { - Utils::invariant( - $e instanceof Exception || $e instanceof Throwable, - 'Expected exception, got %s', - Utils::getVariableType($e) - ); - - $internalErrorMessage = $internalErrorMessage ?: self::$internalErrorMessage; + $internalErrorMessage = $internalErrorMessage ?? self::$internalErrorMessage; - if ($e instanceof ClientAware) { + if ($exception instanceof ClientAware) { $formattedError = [ - 'message' => $e->isClientSafe() ? $e->getMessage() : $internalErrorMessage, + 'message' => $exception->isClientSafe() ? $exception->getMessage() : $internalErrorMessage, 'extensions' => [ - 'category' => $e->getCategory(), + 'category' => $exception->getCategory(), ], ]; } else { @@ -200,26 +191,27 @@ public static function createFromException($e, $debug = false, $internalErrorMes ]; } - if ($e instanceof Error) { + if ($exception instanceof Error) { $locations = Utils::map( - $e->getLocations(), - static function (SourceLocation $loc) { + $exception->getLocations(), + static function (SourceLocation $loc) : array { return $loc->toSerializableArray(); } ); - if (! empty($locations)) { + if (count($locations) > 0) { $formattedError['locations'] = $locations; } - if (! empty($e->path)) { - $formattedError['path'] = $e->path; + + if (count($exception->path ?? []) > 0) { + $formattedError['path'] = $exception->path; } - if (! empty($e->getExtensions())) { - $formattedError['extensions'] = $e->getExtensions() + $formattedError['extensions']; + if (count($exception->getExtensions() ?? []) > 0) { + $formattedError['extensions'] = $exception->getExtensions() + $formattedError['extensions']; } } - if ($debug) { - $formattedError = self::addDebugEntries($formattedError, $e, $debug); + if ($debug !== DebugFlag::NONE) { + $formattedError = self::addDebugEntries($formattedError, $exception, $debug); } return $formattedError; @@ -227,54 +219,42 @@ static function (SourceLocation $loc) { /** * Decorates spec-compliant $formattedError with debug entries according to $debug flags - * (see GraphQL\Error\Debug for available flags) + * (@see \GraphQL\Error\DebugFlag for available flags) * - * @param mixed[] $formattedError - * @param Throwable $e - * @param bool|int $debug + * @param mixed[] $formattedError * * @return mixed[] * * @throws Throwable */ - public static function addDebugEntries(array $formattedError, $e, $debug) + public static function addDebugEntries(array $formattedError, Throwable $e, int $debugFlag) : array { - if (! $debug) { + if ($debugFlag === DebugFlag::NONE) { return $formattedError; } - Utils::invariant( - $e instanceof Exception || $e instanceof Throwable, - 'Expected exception, got %s', - Utils::getVariableType($e) - ); - - $debug = (int) $debug; - - if ($debug & Debug::RETHROW_INTERNAL_EXCEPTIONS) { + if (( $debugFlag & DebugFlag::RETHROW_INTERNAL_EXCEPTIONS) !== 0) { if (! $e instanceof Error) { throw $e; } - if ($e->getPrevious()) { + if ($e->getPrevious() !== null) { throw $e->getPrevious(); } } $isUnsafe = ! $e instanceof ClientAware || ! $e->isClientSafe(); - if (($debug & Debug::RETHROW_UNSAFE_EXCEPTIONS) && $isUnsafe) { - if ($e->getPrevious()) { - throw $e->getPrevious(); - } + if (($debugFlag & DebugFlag::RETHROW_UNSAFE_EXCEPTIONS) !== 0 && $isUnsafe && $e->getPrevious() !== null) { + throw $e->getPrevious(); } - if (($debug & Debug::INCLUDE_DEBUG_MESSAGE) && $isUnsafe) { + if (($debugFlag & DebugFlag::INCLUDE_DEBUG_MESSAGE) !== 0 && $isUnsafe) { // Displaying debugMessage as a first entry: $formattedError = ['debugMessage' => $e->getMessage()] + $formattedError; } - if ($debug & Debug::INCLUDE_TRACE) { + if (($debugFlag & DebugFlag::INCLUDE_TRACE) !== 0) { if ($e instanceof ErrorException || $e instanceof \Error) { $formattedError += [ 'file' => $e->getFile(), @@ -282,10 +262,10 @@ public static function addDebugEntries(array $formattedError, $e, $debug) ]; } - $isTrivial = $e instanceof Error && ! $e->getPrevious(); + $isTrivial = $e instanceof Error && $e->getPrevious() === null; if (! $isTrivial) { - $debugging = $e->getPrevious() ?: $e; + $debugging = $e->getPrevious() ?? $e; $formattedError['trace'] = static::toSafeTrace($debugging); } } @@ -296,18 +276,14 @@ public static function addDebugEntries(array $formattedError, $e, $debug) /** * Prepares final error formatter taking in account $debug flags. * If initial formatter is not set, FormattedError::createFromException is used - * - * @param bool|int $debug - * - * @return callable|callable */ - public static function prepareFormatter(?callable $formatter = null, $debug) + public static function prepareFormatter(?callable $formatter, int $debug) : callable { - $formatter = $formatter ?: static function ($e) { + $formatter = $formatter ?? static function ($e) : array { return FormattedError::createFromException($e); }; - if ($debug) { - $formatter = static function ($e) use ($formatter, $debug) { + if ($debug !== DebugFlag::NONE) { + $formatter = static function ($e) use ($formatter, $debug) : array { return FormattedError::addDebugEntries($formatter($e), $e, $debug); }; } @@ -338,12 +314,12 @@ public static function toSafeTrace($error) } return array_map( - static function ($err) { + static function ($err) : array { $safeErr = array_intersect_key($err, ['file' => true, 'line' => true]); if (isset($err['function'])) { $func = $err['function']; - $args = ! empty($err['args']) ? array_map([self::class, 'printVar'], $err['args']) : []; + $args = array_map([self::class, 'printVar'], $err['args'] ?? []); $funcStr = $func . '(' . implode(', ', $args) . ')'; if (isset($err['class'])) { @@ -412,9 +388,9 @@ public static function create($error, array $locations = []) { $formatted = ['message' => $error]; - if (! empty($locations)) { + if (count($locations) > 0) { $formatted['locations'] = array_map( - static function ($loc) { + static function ($loc) : array { return $loc->toArray(); }, $locations @@ -428,6 +404,8 @@ static function ($loc) { * @deprecated as of v0.10.0, use general purpose method createFromException() instead * * @return mixed[] + * + * @codeCoverageIgnore */ public static function createFromPHPError(ErrorException $e) { diff --git a/src/Error/InvariantViolation.php b/src/Error/InvariantViolation.php index 24d6dbc37..c3cb8abe1 100644 --- a/src/Error/InvariantViolation.php +++ b/src/Error/InvariantViolation.php @@ -13,4 +13,8 @@ */ class InvariantViolation extends LogicException { + public static function shouldNotHappen() : self + { + return new self('This should not have happened'); + } } diff --git a/src/Error/Warning.php b/src/Error/Warning.php index 9157b0554..3efd1cfeb 100644 --- a/src/Error/Warning.php +++ b/src/Error/Warning.php @@ -4,6 +4,8 @@ namespace GraphQL\Error; +use GraphQL\Exception\InvalidArgument; +use function is_int; use function trigger_error; use const E_USER_WARNING; @@ -15,12 +17,12 @@ */ final class Warning { - const WARNING_ASSIGN = 2; - const WARNING_CONFIG = 4; - const WARNING_FULL_SCHEMA_SCAN = 8; - const WARNING_CONFIG_DEPRECATION = 16; - const WARNING_NOT_A_TYPE = 32; - const ALL = 63; + public const WARNING_ASSIGN = 2; + public const WARNING_CONFIG = 4; + public const WARNING_FULL_SCHEMA_SCAN = 8; + public const WARNING_CONFIG_DEPRECATION = 16; + public const WARNING_NOT_A_TYPE = 32; + public const ALL = 63; /** @var int */ private static $enableWarnings = self::ALL; @@ -37,7 +39,7 @@ final class Warning * * @api */ - public static function setWarningHandler(?callable $warningHandler = null) + public static function setWarningHandler(?callable $warningHandler = null) : void { self::$warningHandler = $warningHandler; } @@ -54,14 +56,16 @@ public static function setWarningHandler(?callable $warningHandler = null) * * @api */ - public static function suppress($suppress = true) + public static function suppress($suppress = true) : void { if ($suppress === true) { self::$enableWarnings = 0; } elseif ($suppress === false) { self::$enableWarnings = self::ALL; - } else { + } elseif (is_int($suppress)) { self::$enableWarnings &= ~$suppress; + } else { + throw InvalidArgument::fromExpectedTypeAndArgument('bool|int', $suppress); } } @@ -77,35 +81,41 @@ public static function suppress($suppress = true) * * @api */ - public static function enable($enable = true) + public static function enable($enable = true) : void { if ($enable === true) { self::$enableWarnings = self::ALL; } elseif ($enable === false) { self::$enableWarnings = 0; - } else { + } elseif (is_int($enable)) { self::$enableWarnings |= $enable; + } else { + throw InvalidArgument::fromExpectedTypeAndArgument('bool|int', $enable); } } - public static function warnOnce($errorMessage, $warningId, $messageLevel = null) + public static function warnOnce(string $errorMessage, int $warningId, ?int $messageLevel = null) : void { - if (self::$warningHandler) { + $messageLevel = $messageLevel ?? E_USER_WARNING; + + if (self::$warningHandler !== null) { $fn = self::$warningHandler; - $fn($errorMessage, $warningId); + $fn($errorMessage, $warningId, $messageLevel); } elseif ((self::$enableWarnings & $warningId) > 0 && ! isset(self::$warned[$warningId])) { self::$warned[$warningId] = true; - trigger_error($errorMessage, $messageLevel ?: E_USER_WARNING); + trigger_error($errorMessage, $messageLevel); } } - public static function warn($errorMessage, $warningId, $messageLevel = null) + public static function warn(string $errorMessage, int $warningId, ?int $messageLevel = null) : void { - if (self::$warningHandler) { + $messageLevel = $messageLevel ?? E_USER_WARNING; + + if (self::$warningHandler !== null) { $fn = self::$warningHandler; - $fn($errorMessage, $warningId); + $fn($errorMessage, $warningId, $messageLevel); } elseif ((self::$enableWarnings & $warningId) > 0) { - trigger_error($errorMessage, $messageLevel ?: E_USER_WARNING); + trigger_error($errorMessage, $messageLevel); } } } diff --git a/src/Exception/InvalidArgument.php b/src/Exception/InvalidArgument.php new file mode 100644 index 000000000..eea34d562 --- /dev/null +++ b/src/Exception/InvalidArgument.php @@ -0,0 +1,20 @@ +schema = $schema; $this->fragments = $fragments; - $this->rootValue = $root; + $this->rootValue = $rootValue; $this->contextValue = $contextValue; $this->operation = $operation; - $this->variableValues = $variables; - $this->errors = $errors ?: []; + $this->variableValues = $variableValues; + $this->errors = $errors ?? []; $this->fieldResolver = $fieldResolver; - $this->promises = $promiseAdapter; + $this->promiseAdapter = $promiseAdapter; } public function addError(Error $error) diff --git a/src/Executor/ExecutionResult.php b/src/Executor/ExecutionResult.php index db16dd3d4..4f53287d0 100644 --- a/src/Executor/ExecutionResult.php +++ b/src/Executor/ExecutionResult.php @@ -4,10 +4,12 @@ namespace GraphQL\Executor; +use GraphQL\Error\DebugFlag; use GraphQL\Error\Error; use GraphQL\Error\FormattedError; use JsonSerializable; use function array_map; +use function count; /** * Returned after [query execution](executing-queries.md). @@ -125,35 +127,37 @@ public function jsonSerialize() * If debug argument is passed, output of error formatter is enriched which debugging information * ("debugMessage", "trace" keys depending on flags). * - * $debug argument must be either bool (only adds "debugMessage" to result) or sum of flags from - * GraphQL\Error\Debug - * - * @param bool|int $debug + * $debug argument must sum of flags from @see \GraphQL\Error\DebugFlag * * @return mixed[] * * @api */ - public function toArray($debug = false) + public function toArray(int $debug = DebugFlag::NONE) : array { $result = []; - if (! empty($this->errors)) { - $errorsHandler = $this->errorsHandler ?: static function (array $errors, callable $formatter) { + if (count($this->errors ?? []) > 0) { + $errorsHandler = $this->errorsHandler ?? static function (array $errors, callable $formatter) : array { return array_map($formatter, $errors); }; - $result['errors'] = $errorsHandler( + $handledErrors = $errorsHandler( $this->errors, FormattedError::prepareFormatter($this->errorFormatter, $debug) ); + + // While we know that there were errors initially, they might have been discarded + if ($handledErrors !== []) { + $result['errors'] = $handledErrors; + } } if ($this->data !== null) { $result['data'] = $this->data; } - if (! empty($this->extensions)) { + if (count($this->extensions ?? []) > 0) { $result['extensions'] = $this->extensions; } diff --git a/src/Executor/Executor.php b/src/Executor/Executor.php index 30ccad427..c6040bdfe 100644 --- a/src/Executor/Executor.php +++ b/src/Executor/Executor.php @@ -20,7 +20,7 @@ */ class Executor { - /** @var callable|string[] */ + /** @var callable */ private static $defaultFieldResolver = [self::class, 'defaultFieldResolver']; /** @var PromiseAdapter */ @@ -35,7 +35,7 @@ public static function getDefaultFieldResolver() : callable } /** - * Custom default resolve function. + * Set a custom default resolve function. */ public static function setDefaultFieldResolver(callable $fieldResolver) { @@ -44,9 +44,12 @@ public static function setDefaultFieldResolver(callable $fieldResolver) public static function getPromiseAdapter() : PromiseAdapter { - return self::$defaultPromiseAdapter ?: (self::$defaultPromiseAdapter = new SyncPromiseAdapter()); + return self::$defaultPromiseAdapter ?? (self::$defaultPromiseAdapter = new SyncPromiseAdapter()); } + /** + * Set a custom default promise adapter. + */ public static function setPromiseAdapter(?PromiseAdapter $defaultPromiseAdapter = null) { self::$defaultPromiseAdapter = $defaultPromiseAdapter; @@ -58,9 +61,7 @@ public static function getImplementationFactory() : callable } /** - * Custom executor implementation factory. - * - * Will be called with as + * Set a custom executor implementation factory. */ public static function setImplementationFactory(callable $implementationFactory) { @@ -70,13 +71,13 @@ public static function setImplementationFactory(callable $implementationFactory) /** * Executes DocumentNode against given $schema. * - * Always returns ExecutionResult and never throws. All errors which occur during operation - * execution are collected in `$result->errors`. + * Always returns ExecutionResult and never throws. + * All errors which occur during operation execution are collected in `$result->errors`. * - * @param mixed|null $rootValue - * @param mixed|null $contextValue - * @param mixed[]|ArrayAccess|null $variableValues - * @param string|null $operationName + * @param mixed|null $rootValue + * @param mixed|null $contextValue + * @param array|ArrayAccess|null $variableValues + * @param string|null $operationName * * @return ExecutionResult|Promise * @@ -119,10 +120,10 @@ public static function execute( * * Useful for async PHP platforms. * - * @param mixed|null $rootValue - * @param mixed|null $contextValue - * @param mixed[]|null $variableValues - * @param string|null $operationName + * @param mixed|null $rootValue + * @param mixed|null $contextValue + * @param array|null $variableValues + * @param string|null $operationName * * @return Promise * @@ -149,7 +150,7 @@ public static function promiseToExecute( $contextValue, $variableValues, $operationName, - $fieldResolver ?: self::$defaultFieldResolver + $fieldResolver ?? self::$defaultFieldResolver ); return $executor->doExecute(); @@ -157,31 +158,33 @@ public static function promiseToExecute( /** * If a resolve function is not given, then a default resolve behavior is used - * which takes the property of the source object of the same name as the field + * which takes the property of the root value of the same name as the field * and returns it as the result, or if it's a function, returns the result * of calling that function while passing along args and context. * - * @param mixed $source - * @param mixed[] $args - * @param mixed|null $context + * @param mixed $objectValue + * @param array $args + * @param mixed|null $contextValue * * @return mixed|null */ - public static function defaultFieldResolver($source, $args, $context, ResolveInfo $info) + public static function defaultFieldResolver($objectValue, $args, $contextValue, ResolveInfo $info) { $fieldName = $info->fieldName; $property = null; - if (is_array($source) || $source instanceof ArrayAccess) { - if (isset($source[$fieldName])) { - $property = $source[$fieldName]; + if (is_array($objectValue) || $objectValue instanceof ArrayAccess) { + if (isset($objectValue[$fieldName])) { + $property = $objectValue[$fieldName]; } - } elseif (is_object($source)) { - if (isset($source->{$fieldName})) { - $property = $source->{$fieldName}; + } elseif (is_object($objectValue)) { + if (isset($objectValue->{$fieldName})) { + $property = $objectValue->{$fieldName}; } } - return $property instanceof Closure ? $property($source, $args, $context, $info) : $property; + return $property instanceof Closure + ? $property($objectValue, $args, $contextValue, $info) + : $property; } } diff --git a/src/Executor/Promise/Adapter/AmpPromiseAdapter.php b/src/Executor/Promise/Adapter/AmpPromiseAdapter.php new file mode 100644 index 000000000..e683daefe --- /dev/null +++ b/src/Executor/Promise/Adapter/AmpPromiseAdapter.php @@ -0,0 +1,147 @@ +resolve($value); + } elseif ($onRejected !== null) { + self::resolveWithCallable($deferred, $onRejected, $reason); + } else { + $deferred->fail($reason); + } + }; + + /** @var AmpPromise $adoptedPromise */ + $adoptedPromise = $promise->adoptedPromise; + $adoptedPromise->onResolve($onResolve); + + return new Promise($deferred->promise(), $this); + } + + /** + * @inheritdoc + */ + public function create(callable $resolver) : Promise + { + $deferred = new Deferred(); + + $resolver( + static function ($value) use ($deferred) : void { + $deferred->resolve($value); + }, + static function (Throwable $exception) use ($deferred) : void { + $deferred->fail($exception); + } + ); + + return new Promise($deferred->promise(), $this); + } + + /** + * @inheritdoc + */ + public function createFulfilled($value = null) : Promise + { + $promise = new Success($value); + + return new Promise($promise, $this); + } + + /** + * @inheritdoc + */ + public function createRejected($reason) : Promise + { + $promise = new Failure($reason); + + return new Promise($promise, $this); + } + + /** + * @inheritdoc + */ + public function all(array $promisesOrValues) : Promise + { + /** @var AmpPromise[] $promises */ + $promises = []; + foreach ($promisesOrValues as $key => $item) { + if ($item instanceof Promise) { + $promises[$key] = $item->adoptedPromise; + } elseif ($item instanceof AmpPromise) { + $promises[$key] = $item; + } + } + + $deferred = new Deferred(); + + $onResolve = static function (?Throwable $reason, ?array $values) use ($promisesOrValues, $deferred) : void { + if ($reason === null) { + $deferred->resolve(array_replace($promisesOrValues, $values)); + + return; + } + + $deferred->fail($reason); + }; + + all($promises)->onResolve($onResolve); + + return new Promise($deferred->promise(), $this); + } + + private static function resolveWithCallable(Deferred $deferred, callable $callback, $argument) : void + { + try { + $result = $callback($argument); + } catch (Throwable $exception) { + $deferred->fail($exception); + + return; + } + + if ($result instanceof Promise) { + $result = $result->adoptedPromise; + } + + $deferred->resolve($result); + } +} diff --git a/src/Executor/Promise/Adapter/ReactPromiseAdapter.php b/src/Executor/Promise/Adapter/ReactPromiseAdapter.php index 446d90438..14ff5027e 100644 --- a/src/Executor/Promise/Adapter/ReactPromiseAdapter.php +++ b/src/Executor/Promise/Adapter/ReactPromiseAdapter.php @@ -85,7 +85,7 @@ static function ($item) { } ); - $promise = all($promisesOrValues)->then(static function ($values) use ($promisesOrValues) { + $promise = all($promisesOrValues)->then(static function ($values) use ($promisesOrValues) : array { $orderedResults = []; foreach ($promisesOrValues as $key => $value) { diff --git a/src/Executor/Promise/Adapter/SyncPromise.php b/src/Executor/Promise/Adapter/SyncPromise.php index 26aecd696..d0b94b18d 100644 --- a/src/Executor/Promise/Adapter/SyncPromise.php +++ b/src/Executor/Promise/Adapter/SyncPromise.php @@ -5,7 +5,6 @@ namespace GraphQL\Executor\Promise\Adapter; use Exception; -use GraphQL\Executor\ExecutionResult; use GraphQL\Utils\Utils; use SplQueue; use Throwable; @@ -15,6 +14,14 @@ /** * Simplistic (yet full-featured) implementation of Promises A+ spec for regular PHP `sync` mode * (using queue to defer promises execution) + * + * Note: + * Library users are not supposed to use SyncPromise class in their resolvers. + * Instead they should use GraphQL\Deferred which enforces $executor callback in the constructor. + * + * Root SyncPromise without explicit $executor will never resolve (actually throw while trying). + * The whole point of Deferred is to ensure it never happens and that any resolver creates + * at least one $executor to start the promise chain. */ class SyncPromise { @@ -28,7 +35,7 @@ class SyncPromise /** @var string */ public $state = self::PENDING; - /** @var ExecutionResult|Throwable */ + /** @var mixed */ public $result; /** @@ -38,16 +45,33 @@ class SyncPromise */ private $waiting = []; - public static function runQueue() + public static function runQueue() : void { $q = self::$queue; - while ($q && ! $q->isEmpty()) { + while ($q !== null && ! $q->isEmpty()) { $task = $q->dequeue(); $task(); } } - public function resolve($value) + /** + * @param callable() : mixed $executor + */ + public function __construct(?callable $executor = null) + { + if ($executor === null) { + return; + } + self::getQueue()->enqueue(function () use ($executor) : void { + try { + $this->resolve($executor()); + } catch (Throwable $e) { + $this->reject($e); + } + }); + } + + public function resolve($value) : self { switch ($this->state) { case self::PENDING: @@ -56,10 +80,10 @@ public function resolve($value) } if (is_object($value) && method_exists($value, 'then')) { $value->then( - function ($resolvedValue) { + function ($resolvedValue) : void { $this->resolve($resolvedValue); }, - function ($reason) { + function ($reason) : void { $this->reject($reason); } ); @@ -83,9 +107,9 @@ function ($reason) { return $this; } - public function reject($reason) + public function reject($reason) : self { - if (! $reason instanceof Exception && ! $reason instanceof Throwable) { + if (! $reason instanceof Throwable) { throw new Exception('SyncPromise::reject() has to be called with an instance of \Throwable'); } @@ -107,7 +131,7 @@ public function reject($reason) return $this; } - private function enqueueWaitingPromises() + private function enqueueWaitingPromises() : void { Utils::invariant( $this->state !== self::PENDING, @@ -115,15 +139,13 @@ private function enqueueWaitingPromises() ); foreach ($this->waiting as $descriptor) { - self::getQueue()->enqueue(function () use ($descriptor) { - /** @var $promise self */ + self::getQueue()->enqueue(function () use ($descriptor) : void { + /** @var self $promise */ [$promise, $onFulfilled, $onRejected] = $descriptor; if ($this->state === self::FULFILLED) { try { $promise->resolve($onFulfilled === null ? $this->result : $onFulfilled($this->result)); - } catch (Exception $e) { - $promise->reject($e); } catch (Throwable $e) { $promise->reject($e); } @@ -134,8 +156,6 @@ private function enqueueWaitingPromises() } else { $promise->resolve($onRejected($this->result)); } - } catch (Exception $e) { - $promise->reject($e); } catch (Throwable $e) { $promise->reject($e); } @@ -145,17 +165,21 @@ private function enqueueWaitingPromises() $this->waiting = []; } - public static function getQueue() + public static function getQueue() : SplQueue { - return self::$queue ?: self::$queue = new SplQueue(); + return self::$queue ?? self::$queue = new SplQueue(); } - public function then(?callable $onFulfilled = null, ?callable $onRejected = null) + /** + * @param callable(mixed) : mixed $onFulfilled + * @param callable(Throwable) : mixed $onRejected + */ + public function then(?callable $onFulfilled = null, ?callable $onRejected = null) : self { - if ($this->state === self::REJECTED && ! $onRejected) { + if ($this->state === self::REJECTED && $onRejected === null) { return $this; } - if ($this->state === self::FULFILLED && ! $onFulfilled) { + if ($this->state === self::FULFILLED && $onFulfilled === null) { return $this; } $tmp = new self(); @@ -167,4 +191,12 @@ public function then(?callable $onFulfilled = null, ?callable $onRejected = null return $tmp; } + + /** + * @param callable(Throwable) : mixed $onRejected + */ + public function catch(callable $onRejected) : self + { + return $this->then(null, $onRejected); + } } diff --git a/src/Executor/Promise/Adapter/SyncPromiseAdapter.php b/src/Executor/Promise/Adapter/SyncPromiseAdapter.php index e5900a9d4..f088aec24 100644 --- a/src/Executor/Promise/Adapter/SyncPromiseAdapter.php +++ b/src/Executor/Promise/Adapter/SyncPromiseAdapter.php @@ -4,8 +4,6 @@ namespace GraphQL\Executor\Promise\Adapter; -use Exception; -use GraphQL\Deferred; use GraphQL\Error\InvariantViolation; use GraphQL\Executor\ExecutionResult; use GraphQL\Executor\Promise\Promise; @@ -25,7 +23,7 @@ class SyncPromiseAdapter implements PromiseAdapter */ public function isThenable($value) { - return $value instanceof Deferred; + return $value instanceof SyncPromise; } /** @@ -33,11 +31,12 @@ public function isThenable($value) */ public function convertThenable($thenable) { - if (! $thenable instanceof Deferred) { + if (! $thenable instanceof SyncPromise) { + // End-users should always use Deferred (and don't use SyncPromise directly) throw new InvariantViolation('Expected instance of GraphQL\Deferred, got ' . Utils::printSafe($thenable)); } - return new Promise($thenable->promise, $this); + return new Promise($thenable, $this); } /** @@ -69,8 +68,6 @@ public function create(callable $resolver) 'reject', ] ); - } catch (Exception $e) { - $promise->reject($e); } catch (Throwable $e) { $promise->reject($e); } @@ -113,7 +110,7 @@ public function all(array $promisesOrValues) if ($promiseOrValue instanceof Promise) { $result[$index] = null; $promiseOrValue->then( - static function ($value) use ($index, &$count, $total, &$result, $all) { + static function ($value) use ($index, &$count, $total, &$result, $all) : void { $result[$index] = $value; $count++; if ($count < $total) { @@ -144,13 +141,11 @@ static function ($value) use ($index, &$count, $total, &$result, $all) { public function wait(Promise $promise) { $this->beforeWait($promise); - $dfdQueue = Deferred::getQueue(); - $promiseQueue = SyncPromise::getQueue(); + $taskQueue = SyncPromise::getQueue(); while ($promise->adoptedPromise->state === SyncPromise::PENDING && - ! ($dfdQueue->isEmpty() && $promiseQueue->isEmpty()) + ! $taskQueue->isEmpty() ) { - Deferred::runQueue(); SyncPromise::runQueue(); $this->onWait($promise); } diff --git a/src/Executor/ReferenceExecutor.php b/src/Executor/ReferenceExecutor.php index 7b54afc77..31b862968 100644 --- a/src/Executor/ReferenceExecutor.php +++ b/src/Executor/ReferenceExecutor.php @@ -17,8 +17,9 @@ use GraphQL\Language\AST\FragmentDefinitionNode; use GraphQL\Language\AST\FragmentSpreadNode; use GraphQL\Language\AST\InlineFragmentNode; -use GraphQL\Language\AST\NodeKind; +use GraphQL\Language\AST\Node; use GraphQL\Language\AST\OperationDefinitionNode; +use GraphQL\Language\AST\SelectionNode; use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\Directive; @@ -30,6 +31,7 @@ use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; +use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Introspection; use GraphQL\Type\Schema; use GraphQL\Utils\TypeInfo; @@ -43,9 +45,10 @@ use function array_merge; use function array_reduce; use function array_values; +use function count; use function get_class; use function is_array; -use function is_object; +use function is_callable; use function is_string; use function sprintf; @@ -69,6 +72,11 @@ private function __construct(ExecutionContext $context) $this->subFieldCache = new SplObjectStorage(); } + /** + * @param mixed $rootValue + * @param mixed $contextValue + * @param array|Traversable $variableValues + */ public static function create( PromiseAdapter $promiseAdapter, Schema $schema, @@ -78,7 +86,7 @@ public static function create( $variableValues, ?string $operationName, callable $fieldResolver - ) { + ) : ExecutorImplementation { $exeContext = self::buildExecutionContext( $schema, $documentNode, @@ -115,12 +123,11 @@ public function doExecute() : Promise * Constructs an ExecutionContext object from the arguments passed to * execute, which we will pass throughout the other execution methods. * - * @param mixed $rootValue - * @param mixed $contextValue - * @param mixed[]|Traversable $rawVariableValues - * @param string|null $operationName + * @param mixed $rootValue + * @param mixed $contextValue + * @param array|Traversable $rawVariableValues * - * @return ExecutionContext|Error[] + * @return ExecutionContext|array */ private static function buildExecutionContext( Schema $schema, @@ -128,36 +135,36 @@ private static function buildExecutionContext( $rootValue, $contextValue, $rawVariableValues, - $operationName = null, + ?string $operationName = null, ?callable $fieldResolver = null, ?PromiseAdapter $promiseAdapter = null ) { $errors = []; $fragments = []; - /** @var OperationDefinitionNode $operation */ + /** @var OperationDefinitionNode|null $operation */ $operation = null; $hasMultipleAssumedOperations = false; foreach ($documentNode->definitions as $definition) { - switch ($definition->kind) { - case NodeKind::OPERATION_DEFINITION: - if (! $operationName && $operation) { + switch (true) { + case $definition instanceof OperationDefinitionNode: + if ($operationName === null && $operation !== null) { $hasMultipleAssumedOperations = true; } - if (! $operationName || + if ($operationName === null || (isset($definition->name) && $definition->name->value === $operationName)) { $operation = $definition; } break; - case NodeKind::FRAGMENT_DEFINITION: + case $definition instanceof FragmentDefinitionNode: $fragments[$definition->name->value] = $definition; break; } } if ($operation === null) { - if ($operationName) { - $errors[] = new Error(sprintf('Unknown operation named "%s".', $operationName)); - } else { + if ($operationName === null) { $errors[] = new Error('Must provide an operation.'); + } else { + $errors[] = new Error(sprintf('Unknown operation named "%s".', $operationName)); } } elseif ($hasMultipleAssumedOperations) { $errors[] = new Error( @@ -168,16 +175,16 @@ private static function buildExecutionContext( if ($operation !== null) { [$coercionErrors, $coercedVariableValues] = Values::getVariableValues( $schema, - $operation->variableDefinitions ?: [], - $rawVariableValues ?: [] + $operation->variableDefinitions ?? [], + $rawVariableValues ?? [] ); - if (empty($coercionErrors)) { + if (count($coercionErrors ?? []) === 0) { $variableValues = $coercedVariableValues; } else { $errors = array_merge($errors, $coercionErrors); } } - if (! empty($errors)) { + if (count($errors) > 0) { return $errors; } Utils::invariant($operation, 'Has operation if no errors.'); @@ -199,7 +206,7 @@ private static function buildExecutionContext( public function doExecute() : Promise { // Return a Promise that will eventually resolve to the data described by - // The "Response" section of the GraphQL specification. + // the "Response" section of the GraphQL specification. // // If errors are encountered while executing a GraphQL field, only that // field and its descendants will be omitted, and sibling fields will still @@ -212,7 +219,7 @@ public function doExecute() : Promise // But for the "sync" case it is always fulfilled return $this->isPromise($result) ? $result - : $this->exeContext->promises->createFulfilled($result); + : $this->exeContext->promiseAdapter->createFulfilled($result); } /** @@ -237,9 +244,9 @@ private function buildResponse($data) /** * Implements the "Evaluating operations" section of the spec. * - * @param mixed[] $rootValue + * @param mixed $rootValue * - * @return Promise|stdClass|mixed[] + * @return array|Promise|stdClass|null */ private function executeOperation(OperationDefinitionNode $operation, $rootValue) { @@ -252,16 +259,20 @@ private function executeOperation(OperationDefinitionNode $operation, $rootValue // // Similar to completeValueCatchingError. try { - $result = $operation->operation === 'mutation' ? - $this->executeFieldsSerially($type, $rootValue, $path, $fields) : - $this->executeFields($type, $rootValue, $path, $fields); + $result = $operation->operation === 'mutation' + ? $this->executeFieldsSerially($type, $rootValue, $path, $fields) + : $this->executeFields($type, $rootValue, $path, $fields); if ($this->isPromise($result)) { return $result->then( null, - function ($error) { - $this->exeContext->addError($error); + function ($error) : ?Promise { + if ($error instanceof Error) { + $this->exeContext->addError($error); - return $this->exeContext->promises->createFulfilled(null); + return $this->exeContext->promiseAdapter->createFulfilled(null); + } + + return null; } ); } @@ -277,16 +288,14 @@ function ($error) { /** * Extracts the root type of the operation from the schema. * - * @return ObjectType - * * @throws Error */ - private function getOperationRootType(Schema $schema, OperationDefinitionNode $operation) + private function getOperationRootType(Schema $schema, OperationDefinitionNode $operation) : ObjectType { switch ($operation->operation) { case 'query': $queryType = $schema->getQueryType(); - if (! $queryType) { + if ($queryType === null) { throw new Error( 'Schema does not define the required query root type.', [$operation] @@ -296,7 +305,7 @@ private function getOperationRootType(Schema $schema, OperationDefinitionNode $o return $queryType; case 'mutation': $mutationType = $schema->getMutationType(); - if (! $mutationType) { + if ($mutationType === null) { throw new Error( 'Schema is not configured for mutations.', [$operation] @@ -306,7 +315,7 @@ private function getOperationRootType(Schema $schema, OperationDefinitionNode $o return $mutationType; case 'subscription': $subscriptionType = $schema->getSubscriptionType(); - if (! $subscriptionType) { + if ($subscriptionType === null) { throw new Error( 'Schema is not configured for subscriptions.', [$operation] @@ -329,22 +338,17 @@ private function getOperationRootType(Schema $schema, OperationDefinitionNode $o * CollectFields requires the "runtime type" of an object. For a field which * returns an Interface or Union type, the "runtime type" will be the actual * Object type returned by that field. - * - * @param ArrayObject $fields - * @param ArrayObject $visitedFragmentNames - * - * @return ArrayObject */ private function collectFields( ObjectType $runtimeType, SelectionSetNode $selectionSet, - $fields, - $visitedFragmentNames - ) { + ArrayObject $fields, + ArrayObject $visitedFragmentNames + ) : ArrayObject { $exeContext = $this->exeContext; foreach ($selectionSet->selections as $selection) { - switch ($selection->kind) { - case NodeKind::FIELD: + switch (true) { + case $selection instanceof FieldNode: if (! $this->shouldIncludeNode($selection)) { break; } @@ -354,7 +358,7 @@ private function collectFields( } $fields[$name][] = $selection; break; - case NodeKind::INLINE_FRAGMENT: + case $selection instanceof InlineFragmentNode: if (! $this->shouldIncludeNode($selection) || ! $this->doesFragmentConditionMatch($selection, $runtimeType) ) { @@ -367,15 +371,16 @@ private function collectFields( $visitedFragmentNames ); break; - case NodeKind::FRAGMENT_SPREAD: + case $selection instanceof FragmentSpreadNode: $fragName = $selection->name->value; - if (! empty($visitedFragmentNames[$fragName]) || ! $this->shouldIncludeNode($selection)) { + + if (($visitedFragmentNames[$fragName] ?? false) === true || ! $this->shouldIncludeNode($selection)) { break; } $visitedFragmentNames[$fragName] = true; /** @var FragmentDefinitionNode|null $fragment */ $fragment = $exeContext->fragments[$fragName] ?? null; - if (! $fragment || ! $this->doesFragmentConditionMatch($fragment, $runtimeType)) { + if ($fragment === null || ! $this->doesFragmentConditionMatch($fragment, $runtimeType)) { break; } $this->collectFields( @@ -396,10 +401,8 @@ private function collectFields( * directives, where @skip has higher precedence than @include. * * @param FragmentSpreadNode|FieldNode|InlineFragmentNode $node - * - * @return bool */ - private function shouldIncludeNode($node) + private function shouldIncludeNode(SelectionNode $node) : bool { $variableValues = $this->exeContext->variableValues; $skipDirective = Directive::skipDirective(); @@ -423,25 +426,19 @@ private function shouldIncludeNode($node) /** * Implements the logic to compute the key of a given fields entry - * - * @return string */ - private static function getFieldEntryKey(FieldNode $node) + private static function getFieldEntryKey(FieldNode $node) : string { - return $node->alias ? $node->alias->value : $node->name->value; + return $node->alias === null ? $node->name->value : $node->alias->value; } /** * Determines if a fragment is applicable to the given type. * * @param FragmentDefinitionNode|InlineFragmentNode $fragment - * - * @return bool */ - private function doesFragmentConditionMatch( - $fragment, - ObjectType $type - ) { + private function doesFragmentConditionMatch(Node $fragment, ObjectType $type) : bool + { $typeConditionNode = $fragment->typeCondition; if ($typeConditionNode === null) { return true; @@ -451,7 +448,7 @@ private function doesFragmentConditionMatch( return true; } if ($conditionalType instanceof AbstractType) { - return $this->exeContext->schema->isPossibleType($conditionalType, $type); + return $this->exeContext->schema->isSubType($conditionalType, $type); } return false; @@ -461,26 +458,25 @@ private function doesFragmentConditionMatch( * Implements the "Evaluating selection sets" section of the spec * for "write" mode. * - * @param mixed[] $sourceValue - * @param mixed[] $path - * @param ArrayObject $fields + * @param mixed $rootValue + * @param array $path * - * @return Promise|stdClass|mixed[] + * @return array|Promise|stdClass */ - private function executeFieldsSerially(ObjectType $parentType, $sourceValue, $path, $fields) + private function executeFieldsSerially(ObjectType $parentType, $rootValue, array $path, ArrayObject $fields) { $result = $this->promiseReduce( array_keys($fields->getArrayCopy()), - function ($results, $responseName) use ($path, $parentType, $sourceValue, $fields) { + function ($results, $responseName) use ($path, $parentType, $rootValue, $fields) { $fieldNodes = $fields[$responseName]; $fieldPath = $path; $fieldPath[] = $responseName; - $result = $this->resolveField($parentType, $sourceValue, $fieldNodes, $fieldPath); + $result = $this->resolveField($parentType, $rootValue, $fieldNodes, $fieldPath); if ($result === self::$UNDEFINED) { return $results; } $promise = $this->getPromise($result); - if ($promise) { + if ($promise !== null) { return $promise->then(static function ($resolvedResult) use ($responseName, $results) { $results[$responseName] = $resolvedResult; @@ -493,6 +489,7 @@ function ($results, $responseName) use ($path, $parentType, $sourceValue, $field }, [] ); + if ($this->isPromise($result)) { return $result->then(static function ($resolvedResults) { return self::fixResultsIfEmptyArray($resolvedResults); @@ -503,33 +500,35 @@ function ($results, $responseName) use ($path, $parentType, $sourceValue, $field } /** - * Resolves the field on the given source object. In particular, this - * figures out the value that the field returns by calling its resolve function, - * then calls completeValue to complete promises, serialize scalars, or execute - * the sub-selection-set for objects. + * Resolves the field on the given root value. + * + * In particular, this figures out the value that the field returns + * by calling its resolve function, then calls completeValue to complete promises, + * serialize scalars, or execute the sub-selection-set for objects. * - * @param object|null $source - * @param FieldNode[] $fieldNodes - * @param mixed[] $path + * @param mixed $rootValue + * @param array $path * - * @return mixed[]|Exception|mixed|null + * @return array|Throwable|mixed|null */ - private function resolveField(ObjectType $parentType, $source, $fieldNodes, $path) + private function resolveField(ObjectType $parentType, $rootValue, ArrayObject $fieldNodes, array $path) { $exeContext = $this->exeContext; $fieldNode = $fieldNodes[0]; $fieldName = $fieldNode->name->value; $fieldDef = $this->getFieldDef($exeContext->schema, $parentType, $fieldName); - if (! $fieldDef) { + if ($fieldDef === null) { return self::$UNDEFINED; } $returnType = $fieldDef->getType(); - // The resolve function's optional third argument is a collection of + // The resolve function's optional 3rd argument is a context value that + // is provided to every resolve function within an execution. It is commonly + // used to represent an authenticated user, or request-specific caches. + // The resolve function's optional 4th argument is a collection of // information about the current execution state. $info = new ResolveInfo( - $fieldName, + $fieldDef, $fieldNodes, - $returnType, $parentType, $path, $exeContext->schema, @@ -545,18 +544,13 @@ private function resolveField(ObjectType $parentType, $source, $fieldNodes, $pat } else { $resolveFn = $this->exeContext->fieldResolver; } - // The resolve function's optional third argument is a context value that - // is provided to every resolve function within an execution. It is commonly - // used to represent an authenticated user, or request-specific caches. - $context = $exeContext->contextValue; // Get the resolve function, regardless of if its result is normal // or abrupt (error). - $result = $this->resolveOrError( + $result = $this->resolveFieldValueOrError( $fieldDef, $fieldNode, $resolveFn, - $source, - $context, + $rootValue, $info ); $result = $this->completeValueCatchingError( @@ -572,23 +566,20 @@ private function resolveField(ObjectType $parentType, $source, $fieldNodes, $pat /** * This method looks up the field on the given type definition. + * * It has special casing for the two introspection fields, __schema * and __typename. __typename is special because it can always be * queried as a field, even in situations where no other fields * are allowed, like on a Union. __schema could get automatically * added to the query type, but that would require mutating type * definitions, which would cause issues. - * - * @param string $fieldName - * - * @return FieldDefinition */ - private function getFieldDef(Schema $schema, ObjectType $parentType, $fieldName) + private function getFieldDef(Schema $schema, ObjectType $parentType, string $fieldName) : ?FieldDefinition { static $schemaMetaFieldDef, $typeMetaFieldDef, $typeNameMetaFieldDef; - $schemaMetaFieldDef = $schemaMetaFieldDef ?: Introspection::schemaMetaFieldDef(); - $typeMetaFieldDef = $typeMetaFieldDef ?: Introspection::typeMetaFieldDef(); - $typeNameMetaFieldDef = $typeNameMetaFieldDef ?: Introspection::typeNameMetaFieldDef(); + $schemaMetaFieldDef = $schemaMetaFieldDef ?? Introspection::schemaMetaFieldDef(); + $typeMetaFieldDef = $typeMetaFieldDef ?? Introspection::typeMetaFieldDef(); + $typeNameMetaFieldDef = $typeNameMetaFieldDef ?? Introspection::typeNameMetaFieldDef(); if ($fieldName === $schemaMetaFieldDef->name && $schema->getQueryType() === $parentType) { return $schemaMetaFieldDef; } @@ -606,32 +597,31 @@ private function getFieldDef(Schema $schema, ObjectType $parentType, $fieldName) } /** - * Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField` - * function. Returns the result of resolveFn or the abrupt-return Error object. + * Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField` function. + * Returns the result of resolveFn or the abrupt-return Error object. * - * @param FieldDefinition $fieldDef - * @param FieldNode $fieldNode - * @param callable $resolveFn - * @param mixed $source - * @param mixed $context - * @param ResolveInfo $info + * @param mixed $rootValue * * @return Throwable|Promise|mixed */ - private function resolveOrError($fieldDef, $fieldNode, $resolveFn, $source, $context, $info) - { + private function resolveFieldValueOrError( + FieldDefinition $fieldDef, + FieldNode $fieldNode, + callable $resolveFn, + $rootValue, + ResolveInfo $info + ) { try { - // Build hash of arguments from the field.arguments AST, using the + // Build a map of arguments from the field.arguments AST, using the // variables scope to fulfill any variable references. - $args = Values::getArgumentValues( + $args = Values::getArgumentValues( $fieldDef, $fieldNode, $this->exeContext->variableValues ); + $contextValue = $this->exeContext->contextValue; - return $resolveFn($source, $args, $context, $info); - } catch (Exception $error) { - return $error; + return $resolveFn($rootValue, $args, $contextValue, $info); } catch (Throwable $error) { return $error; } @@ -641,110 +631,67 @@ private function resolveOrError($fieldDef, $fieldNode, $resolveFn, $source, $con * This is a small wrapper around completeValue which detects and logs errors * in the execution context. * - * @param FieldNode[] $fieldNodes - * @param string[] $path - * @param mixed $result + * @param array $path + * @param mixed $result * - * @return mixed[]|Promise|null + * @return array|Promise|stdClass|null */ private function completeValueCatchingError( Type $returnType, - $fieldNodes, + ArrayObject $fieldNodes, ResolveInfo $info, - $path, + array $path, $result ) { - $exeContext = $this->exeContext; - // If the field type is non-nullable, then it is resolved without any - // protection from errors. - if ($returnType instanceof NonNull) { - return $this->completeValueWithLocatedError( - $returnType, - $fieldNodes, - $info, - $path, - $result - ); - } // Otherwise, error protection is applied, logging the error and resolving // a null value for this field if one is encountered. try { - $completed = $this->completeValueWithLocatedError( - $returnType, - $fieldNodes, - $info, - $path, - $result - ); - $promise = $this->getPromise($completed); - if ($promise) { - return $promise->then( - null, - function ($error) use ($exeContext) { - $exeContext->addError($error); + $promise = $this->getPromise($result); + if ($promise !== null) { + $completed = $promise->then(function (&$resolved) use ($returnType, $fieldNodes, $info, $path) { + return $this->completeValue($returnType, $fieldNodes, $info, $path, $resolved); + }); + } else { + $completed = $this->completeValue($returnType, $fieldNodes, $info, $path, $result); + } - return $this->exeContext->promises->createFulfilled(null); - } - ); + $promise = $this->getPromise($completed); + if ($promise !== null) { + return $promise->then(null, function ($error) use ($fieldNodes, $path, $returnType) : void { + $this->handleFieldError($error, $fieldNodes, $path, $returnType); + }); } return $completed; - } catch (Error $err) { - // If `completeValueWithLocatedError` returned abruptly (threw an error), log the error - // and return null. - $exeContext->addError($err); + } catch (Throwable $err) { + $this->handleFieldError($err, $fieldNodes, $path, $returnType); return null; } } /** - * This is a small wrapper around completeValue which annotates errors with - * location information. - * - * @param FieldNode[] $fieldNodes - * @param string[] $path - * @param mixed $result - * - * @return mixed[]|mixed|Promise|null + * @param mixed $rawError + * @param array $path * * @throws Error */ - public function completeValueWithLocatedError( - Type $returnType, - $fieldNodes, - ResolveInfo $info, - $path, - $result - ) { - try { - $completed = $this->completeValue( - $returnType, - $fieldNodes, - $info, - $path, - $result - ); - $promise = $this->getPromise($completed); - if ($promise) { - return $promise->then( - null, - function ($error) use ($fieldNodes, $path) { - return $this->exeContext->promises->createRejected(Error::createLocatedError( - $error, - $fieldNodes, - $path - )); - } - ); - } + private function handleFieldError($rawError, ArrayObject $fieldNodes, array $path, Type $returnType) : void + { + $error = Error::createLocatedError( + $rawError, + $fieldNodes, + $path + ); - return $completed; - } catch (Exception $error) { - throw Error::createLocatedError($error, $fieldNodes, $path); - } catch (Throwable $error) { - throw Error::createLocatedError($error, $fieldNodes, $path); + // If the field type is non-nullable, then it is resolved without any + // protection from errors, however it still properly locates the error. + if ($returnType instanceof NonNull) { + throw $error; } + // Otherwise, error protection is applied, logging the error and resolving + // a null value for this field if one is encountered. + $this->exeContext->addError($error); } /** @@ -768,32 +715,26 @@ function ($error) use ($fieldNodes, $path) { * Otherwise, the field type expects a sub-selection set, and will complete the * value by evaluating all sub-selections. * - * @param FieldNode[] $fieldNodes - * @param string[] $path - * @param mixed $result + * @param array $path + * @param mixed $result * - * @return mixed[]|mixed|Promise|null + * @return array|mixed|Promise|null * * @throws Error * @throws Throwable */ private function completeValue( Type $returnType, - $fieldNodes, + ArrayObject $fieldNodes, ResolveInfo $info, - $path, + array $path, &$result ) { - $promise = $this->getPromise($result); - // If result is a Promise, apply-lift over completeValue. - if ($promise) { - return $promise->then(function (&$resolved) use ($returnType, $fieldNodes, $info, $path) { - return $this->completeValue($returnType, $fieldNodes, $info, $path, $resolved); - }); - } - if ($result instanceof Exception || $result instanceof Throwable) { + // If result is an Error, throw a located error. + if ($result instanceof Throwable) { throw $result; } + // If field type is NonNull, complete for inner type, and throw field error // if result is null. if ($returnType instanceof NonNull) { @@ -806,7 +747,7 @@ private function completeValue( ); if ($completed === null) { throw new InvariantViolation( - 'Cannot return null for non-nullable field ' . $info->parentType . '.' . $info->fieldName . '.' + sprintf('Cannot return null for non-nullable field "%s.%s".', $info->parentType, $info->fieldName) ); } @@ -824,7 +765,7 @@ private function completeValue( // instance than `resolveType` or $field->getType() or $arg->getType() if ($returnType !== $this->exeContext->schema->getType($returnType->name)) { $hint = ''; - if ($this->exeContext->schema->getConfig()->typeLoader) { + if ($this->exeContext->schema->getConfig()->typeLoader !== null) { $hint = sprintf( 'Make sure that type loader returns the same instance as defined in %s.%s', $info->parentType, @@ -857,12 +798,10 @@ private function completeValue( /** * @param mixed $value - * - * @return bool */ - private function isPromise($value) + private function isPromise($value) : bool { - return $value instanceof Promise || $this->exeContext->promises->isThenable($value); + return $value instanceof Promise || $this->exeContext->promiseAdapter->isThenable($value); } /** @@ -870,20 +809,18 @@ private function isPromise($value) * otherwise returns null. * * @param mixed $value - * - * @return Promise|null */ - private function getPromise($value) + private function getPromise($value) : ?Promise { if ($value === null || $value instanceof Promise) { return $value; } - if ($this->exeContext->promises->isThenable($value)) { - $promise = $this->exeContext->promises->convertThenable($value); + if ($this->exeContext->promiseAdapter->isThenable($value)) { + $promise = $this->exeContext->promiseAdapter->convertThenable($value); if (! $promise instanceof Promise) { throw new InvariantViolation(sprintf( '%s::convertThenable is expected to return instance of GraphQL\Executor\Promise\Promise, got: %s', - get_class($this->exeContext->promises), + get_class($this->exeContext->promiseAdapter), Utils::printSafe($promise) )); } @@ -901,10 +838,10 @@ private function getPromise($value) * If the callback does not return a Promise, then this function will also not * return a Promise. * - * @param mixed[] $values + * @param array $values * @param Promise|mixed|null $initialValue * - * @return mixed[] + * @return Promise|mixed|null */ private function promiseReduce(array $values, callable $callback, $initialValue) { @@ -912,7 +849,7 @@ private function promiseReduce(array $values, callable $callback, $initialValue) $values, function ($previous, $value) use ($callback) { $promise = $this->getPromise($previous); - if ($promise) { + if ($promise !== null) { return $promise->then(static function ($resolved) use ($callback, $value) { return $callback($resolved, $value); }); @@ -925,44 +862,45 @@ function ($previous, $value) use ($callback) { } /** - * Complete a list value by completing each item in the list with the - * inner type + * Complete a list value by completing each item in the list with the inner type. * - * @param FieldNode[] $fieldNodes - * @param mixed[] $path - * @param mixed $result + * @param array $path + * @param array|Traversable $results * - * @return mixed[]|Promise + * @return array|Promise|stdClass * * @throws Exception */ - private function completeListValue(ListOfType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result) + private function completeListValue(ListOfType $returnType, ArrayObject $fieldNodes, ResolveInfo $info, array $path, &$results) { $itemType = $returnType->getWrappedType(); Utils::invariant( - is_array($result) || $result instanceof Traversable, + is_array($results) || $results instanceof Traversable, 'User Error: expected iterable, but did not find one for field ' . $info->parentType . '.' . $info->fieldName . '.' ); $containsPromise = false; $i = 0; $completedItems = []; - foreach ($result as $item) { + foreach ($results as $item) { $fieldPath = $path; $fieldPath[] = $i++; + $info->path = $fieldPath; $completedItem = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $fieldPath, $item); - if (! $containsPromise && $this->getPromise($completedItem)) { + if (! $containsPromise && $this->getPromise($completedItem) !== null) { $containsPromise = true; } $completedItems[] = $completedItem; } - return $containsPromise ? $this->exeContext->promises->all($completedItems) : $completedItems; + return $containsPromise + ? $this->exeContext->promiseAdapter->all($completedItems) + : $completedItems; } /** * Complete a Scalar or Enum by serializing to a valid value, throwing if serialization is not possible. * - * @param mixed $result + * @param mixed $result * * @return mixed * @@ -972,12 +910,6 @@ private function completeLeafValue(LeafType $returnType, &$result) { try { return $returnType->serialize($result); - } catch (Exception $error) { - throw new InvariantViolation( - 'Expected a value of type "' . Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result), - 0, - $error - ); } catch (Throwable $error) { throw new InvariantViolation( 'Expected a value of type "' . Utils::printSafe($returnType) . '" but received: ' . Utils::printSafe($result), @@ -991,23 +923,32 @@ private function completeLeafValue(LeafType $returnType, &$result) * Complete a value of an abstract type by determining the runtime object type * of that value, then complete the value for that type. * - * @param FieldNode[] $fieldNodes - * @param mixed[] $path - * @param mixed[] $result + * @param array $path + * @param array $result * - * @return mixed + * @return array|Promise|stdClass * * @throws Error */ - private function completeAbstractValue(AbstractType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result) - { - $exeContext = $this->exeContext; - $runtimeType = $returnType->resolveType($result, $exeContext->contextValue, $info); - if ($runtimeType === null) { + private function completeAbstractValue( + AbstractType $returnType, + ArrayObject $fieldNodes, + ResolveInfo $info, + array $path, + &$result + ) { + $exeContext = $this->exeContext; + $typeCandidate = $returnType->resolveType($result, $exeContext->contextValue, $info); + + if ($typeCandidate === null) { $runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType); + } elseif (is_callable($typeCandidate)) { + $runtimeType = Schema::resolveType($typeCandidate); + } else { + $runtimeType = $typeCandidate; } $promise = $this->getPromise($runtimeType); - if ($promise) { + if ($promise !== null) { return $promise->then(function ($resolvedRuntimeType) use ( $returnType, $fieldNodes, @@ -1054,12 +995,13 @@ private function completeAbstractValue(AbstractType $returnType, $fieldNodes, Re * Otherwise, test each possible type for the abstract type by calling * isTypeOf for the object being coerced, returning the first type that matches. * - * @param mixed|null $value - * @param mixed|null $context + * @param mixed|null $value + * @param mixed|null $contextValue + * @param InterfaceType|UnionType $abstractType * - * @return ObjectType|Promise|null + * @return Promise|Type|string|null */ - private function defaultTypeResolver($value, $context, ResolveInfo $info, AbstractType $abstractType) + private function defaultTypeResolver($value, $contextValue, ResolveInfo $info, AbstractType $abstractType) { // First, look for `__typename`. if ($value !== null && @@ -1069,7 +1011,8 @@ private function defaultTypeResolver($value, $context, ResolveInfo $info, Abstra ) { return $value['__typename']; } - if ($abstractType instanceof InterfaceType && $info->schema->getConfig()->typeLoader) { + + if ($abstractType instanceof InterfaceType && $info->schema->getConfig()->typeLoader !== null) { Warning::warnOnce( sprintf( 'GraphQL Interface Type `%s` returned `null` from its `resolveType` function ' . @@ -1086,20 +1029,20 @@ private function defaultTypeResolver($value, $context, ResolveInfo $info, Abstra $possibleTypes = $info->schema->getPossibleTypes($abstractType); $promisedIsTypeOfResults = []; foreach ($possibleTypes as $index => $type) { - $isTypeOfResult = $type->isTypeOf($value, $context, $info); + $isTypeOfResult = $type->isTypeOf($value, $contextValue, $info); if ($isTypeOfResult === null) { continue; } $promise = $this->getPromise($isTypeOfResult); - if ($promise) { + if ($promise !== null) { $promisedIsTypeOfResults[$index] = $promise; } elseif ($isTypeOfResult) { return $type; } } - if (! empty($promisedIsTypeOfResults)) { - return $this->exeContext->promises->all($promisedIsTypeOfResults) - ->then(static function ($isTypeOfResults) use ($possibleTypes) { + if (count($promisedIsTypeOfResults) > 0) { + return $this->exeContext->promiseAdapter->all($promisedIsTypeOfResults) + ->then(static function ($isTypeOfResults) use ($possibleTypes) : ?ObjectType { foreach ($isTypeOfResults as $index => $result) { if ($result) { return $possibleTypes[$index]; @@ -1116,23 +1059,27 @@ private function defaultTypeResolver($value, $context, ResolveInfo $info, Abstra /** * Complete an Object value by executing all sub-selections. * - * @param FieldNode[] $fieldNodes - * @param mixed[] $path - * @param mixed $result + * @param array $path + * @param mixed $result * - * @return mixed[]|Promise|stdClass + * @return array|Promise|stdClass * * @throws Error */ - private function completeObjectValue(ObjectType $returnType, $fieldNodes, ResolveInfo $info, $path, &$result) - { + private function completeObjectValue( + ObjectType $returnType, + ArrayObject $fieldNodes, + ResolveInfo $info, + array $path, + &$result + ) { // If there is an isTypeOf predicate function, call it with the // current result. If isTypeOf returns false, then raise an error rather // than continuing execution. $isTypeOf = $returnType->isTypeOf($result, $this->exeContext->contextValue, $info); if ($isTypeOf !== null) { $promise = $this->getPromise($isTypeOf); - if ($promise) { + if ($promise !== null) { return $promise->then(function ($isTypeOfResult) use ( $returnType, $fieldNodes, @@ -1165,15 +1112,14 @@ private function completeObjectValue(ObjectType $returnType, $fieldNodes, Resolv } /** - * @param mixed[] $result - * @param FieldNode[] $fieldNodes + * @param array $result * * @return Error */ private function invalidReturnTypeError( ObjectType $returnType, $result, - $fieldNodes + ArrayObject $fieldNodes ) { return new Error( 'Expected value of type "' . $returnType->name . '" but got: ' . Utils::printSafe($result) . '.', @@ -1182,18 +1128,17 @@ private function invalidReturnTypeError( } /** - * @param FieldNode[] $fieldNodes - * @param mixed[] $path - * @param mixed[] $result + * @param array $path + * @param mixed $result * - * @return mixed[]|Promise|stdClass + * @return array|Promise|stdClass * * @throws Error */ private function collectAndExecuteSubfields( ObjectType $returnType, - $fieldNodes, - $path, + ArrayObject $fieldNodes, + array $path, &$result ) { $subFieldNodes = $this->collectSubFields($returnType, $fieldNodes); @@ -1201,7 +1146,12 @@ private function collectAndExecuteSubfields( return $this->executeFields($returnType, $result, $path, $subFieldNodes); } - private function collectSubFields(ObjectType $returnType, $fieldNodes) : ArrayObject + /** + * A memoized collection of relevant subfields with regard to the return + * type. Memoizing ensures the subfields are not repeatedly calculated, which + * saves overhead when resolving lists of values. + */ + private function collectSubFields(ObjectType $returnType, ArrayObject $fieldNodes) : ArrayObject { if (! isset($this->subFieldCache[$returnType])) { $this->subFieldCache[$returnType] = new SplObjectStorage(); @@ -1231,46 +1181,46 @@ private function collectSubFields(ObjectType $returnType, $fieldNodes) : ArrayOb * Implements the "Evaluating selection sets" section of the spec * for "read" mode. * - * @param mixed|null $source - * @param mixed[] $path - * @param ArrayObject $fields + * @param mixed $rootValue + * @param array $path * - * @return Promise|stdClass|mixed[] + * @return Promise|stdClass|array */ - private function executeFields(ObjectType $parentType, $source, $path, $fields) + private function executeFields(ObjectType $parentType, $rootValue, array $path, ArrayObject $fields) { $containsPromise = false; - $finalResults = []; + $results = []; foreach ($fields as $responseName => $fieldNodes) { $fieldPath = $path; $fieldPath[] = $responseName; - $result = $this->resolveField($parentType, $source, $fieldNodes, $fieldPath); + $result = $this->resolveField($parentType, $rootValue, $fieldNodes, $fieldPath); if ($result === self::$UNDEFINED) { continue; } - if (! $containsPromise && $this->getPromise($result)) { + if (! $containsPromise && $this->isPromise($result)) { $containsPromise = true; } - $finalResults[$responseName] = $result; + $results[$responseName] = $result; } // If there are no promises, we can just return the object if (! $containsPromise) { - return self::fixResultsIfEmptyArray($finalResults); + return self::fixResultsIfEmptyArray($results); } - // Otherwise, results is a map from field name to the result - // of resolving that field, which is possibly a promise. Return - // a promise that will return this same map, but with any - // promises replaced with the values they resolved to. - return $this->promiseForAssocArray($finalResults); + // Otherwise, results is a map from field name to the result of resolving that + // field, which is possibly a promise. Return a promise that will return this + // same map, but with any promises replaced with the values they resolved to. + return $this->promiseForAssocArray($results); } /** + * Differentiate empty objects from empty lists. + * * @see https://github.com/webonyx/graphql-php/issues/59 * - * @param mixed[] $results + * @param array|mixed $results * - * @return stdClass|mixed[] + * @return array|stdClass|mixed */ private static function fixResultsIfEmptyArray($results) { @@ -1282,21 +1232,16 @@ private static function fixResultsIfEmptyArray($results) } /** - * This function transforms a PHP `array` into - * a `Promise>` - * - * In other words it returns a promise which resolves to normal PHP associative array which doesn't contain - * any promises. - * - * @param (string|Promise)[] $assoc + * Transform an associative array with Promises to a Promise which resolves to an + * associative array where all Promises were resolved. * - * @return mixed + * @param array $assoc */ - private function promiseForAssocArray(array $assoc) + private function promiseForAssocArray(array $assoc) : Promise { $keys = array_keys($assoc); $valuesAndPromises = array_values($assoc); - $promise = $this->exeContext->promises->all($valuesAndPromises); + $promise = $this->exeContext->promiseAdapter->all($valuesAndPromises); return $promise->then(static function ($values) use ($keys) { $resolvedResults = []; @@ -1309,21 +1254,19 @@ private function promiseForAssocArray(array $assoc) } /** - * @param string|ObjectType|null $runtimeTypeOrName - * @param FieldNode[] $fieldNodes - * @param mixed $result - * - * @return ObjectType + * @param string|ObjectType|null $runtimeTypeOrName + * @param InterfaceType|UnionType $returnType + * @param mixed $result */ private function ensureValidRuntimeType( $runtimeTypeOrName, AbstractType $returnType, ResolveInfo $info, &$result - ) { - $runtimeType = is_string($runtimeTypeOrName) ? - $this->exeContext->schema->getType($runtimeTypeOrName) : - $runtimeTypeOrName; + ) : ObjectType { + $runtimeType = is_string($runtimeTypeOrName) + ? $this->exeContext->schema->getType($runtimeTypeOrName) + : $runtimeTypeOrName; if (! $runtimeType instanceof ObjectType) { throw new InvariantViolation( sprintf( @@ -1340,7 +1283,7 @@ private function ensureValidRuntimeType( ) ); } - if (! $this->exeContext->schema->isPossibleType($returnType, $runtimeType)) { + if (! $this->exeContext->schema->isSubType($returnType, $runtimeType)) { throw new InvariantViolation( sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeType, $returnType) ); diff --git a/src/Executor/Values.php b/src/Executor/Values.php index 2f74e8c54..ae8d63f37 100644 --- a/src/Executor/Values.php +++ b/src/Executor/Values.php @@ -6,22 +6,34 @@ use GraphQL\Error\Error; use GraphQL\Language\AST\ArgumentNode; +use GraphQL\Language\AST\BooleanValueNode; use GraphQL\Language\AST\DirectiveNode; use GraphQL\Language\AST\EnumValueDefinitionNode; +use GraphQL\Language\AST\EnumValueNode; use GraphQL\Language\AST\FieldDefinitionNode; use GraphQL\Language\AST\FieldNode; +use GraphQL\Language\AST\FloatValueNode; use GraphQL\Language\AST\FragmentSpreadNode; use GraphQL\Language\AST\InlineFragmentNode; +use GraphQL\Language\AST\IntValueNode; +use GraphQL\Language\AST\ListValueNode; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeList; +use GraphQL\Language\AST\NullValueNode; +use GraphQL\Language\AST\ObjectValueNode; +use GraphQL\Language\AST\StringValueNode; use GraphQL\Language\AST\ValueNode; use GraphQL\Language\AST\VariableDefinitionNode; use GraphQL\Language\AST\VariableNode; use GraphQL\Language\Printer; use GraphQL\Type\Definition\Directive; +use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\FieldDefinition; +use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputType; +use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; +use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; use GraphQL\Utils\AST; @@ -32,6 +44,7 @@ use Throwable; use function array_key_exists; use function array_map; +use function count; use function sprintf; class Values @@ -55,48 +68,9 @@ public static function getVariableValues(Schema $schema, $varDefNodes, array $in /** @var InputType|Type $varType */ $varType = TypeInfo::typeFromAST($schema, $varDefNode->type); - if (Type::isInputType($varType)) { - if (array_key_exists($varName, $inputs)) { - $value = $inputs[$varName]; - $coerced = Value::coerceValue($value, $varType, $varDefNode); - /** @var Error[] $coercionErrors */ - $coercionErrors = $coerced['errors']; - if (empty($coercionErrors)) { - $coercedValues[$varName] = $coerced['value']; - } else { - $messagePrelude = sprintf( - 'Variable "$%s" got invalid value %s; ', - $varName, - Utils::printSafeJson($value) - ); - - foreach ($coercionErrors as $error) { - $errors[] = new Error( - $messagePrelude . $error->getMessage(), - $error->getNodes(), - $error->getSource(), - $error->getPositions(), - $error->getPath(), - $error, - $error->getExtensions() - ); - } - } - } else { - if ($varType instanceof NonNull) { - $errors[] = new Error( - sprintf( - 'Variable "$%s" of required type "%s" was not provided.', - $varName, - $varType - ), - [$varDefNode] - ); - } elseif ($varDefNode->defaultValue) { - $coercedValues[$varName] = AST::valueFromAST($varDefNode->defaultValue, $varType); - } - } - } else { + if (! Type::isInputType($varType)) { + // Must use input types for variables. This should be caught during + // validation, however is checked again here for safety. $errors[] = new Error( sprintf( 'Variable "$%s" expected value of type "%s" which cannot be used as an input type.', @@ -105,10 +79,65 @@ public static function getVariableValues(Schema $schema, $varDefNodes, array $in ), [$varDefNode->type] ); + } else { + $hasValue = array_key_exists($varName, $inputs); + $value = $hasValue ? $inputs[$varName] : Utils::undefined(); + + if (! $hasValue && ($varDefNode->defaultValue !== null)) { + // If no value was provided to a variable with a default value, + // use the default value. + $coercedValues[$varName] = AST::valueFromAST($varDefNode->defaultValue, $varType); + } elseif ((! $hasValue || $value === null) && ($varType instanceof NonNull)) { + // If no value or a nullish value was provided to a variable with a + // non-null type (required), produce an error. + $errors[] = new Error( + sprintf( + $hasValue + ? 'Variable "$%s" of non-null type "%s" must not be null.' + : 'Variable "$%s" of required type "%s" was not provided.', + $varName, + Utils::printSafe($varType) + ), + [$varDefNode] + ); + } elseif ($hasValue) { + if ($value === null) { + // If the explicit value `null` was provided, an entry in the coerced + // values must exist as the value `null`. + $coercedValues[$varName] = null; + } else { + // Otherwise, a non-null value was provided, coerce it to the expected + // type or report an error if coercion fails. + $coerced = Value::coerceValue($value, $varType, $varDefNode); + /** @var Error[] $coercionErrors */ + $coercionErrors = $coerced['errors']; + if (count($coercionErrors ?? []) > 0) { + $messagePrelude = sprintf( + 'Variable "$%s" got invalid value %s; ', + $varName, + Utils::printSafeJson($value) + ); + + foreach ($coercionErrors as $error) { + $errors[] = new Error( + $messagePrelude . $error->getMessage(), + $error->getNodes(), + $error->getSource(), + $error->getPositions(), + $error->getPath(), + $error->getPrevious(), + $error->getExtensions() + ); + } + } else { + $coercedValues[$varName] = $coerced['value']; + } + } + } } } - if (! empty($errors)) { + if (count($errors) > 0) { return [$errors, null]; } @@ -132,7 +161,7 @@ public static function getDirectiveValues(Directive $directiveDef, $node, $varia if (isset($node->directives) && $node->directives instanceof NodeList) { $directiveNode = Utils::find( $node->directives, - static function (DirectiveNode $directive) use ($directiveDef) { + static function (DirectiveNode $directive) use ($directiveDef) : bool { return $directive->name->value === $directiveDef->name; } ); @@ -159,15 +188,11 @@ static function (DirectiveNode $directive) use ($directiveDef) { */ public static function getArgumentValues($def, $node, $variableValues = null) { - if (empty($def->args)) { - return []; - } - - $argumentNodes = $node->arguments; - if (empty($argumentNodes)) { + if (count($def->args) === 0) { return []; } + $argumentNodes = $node->arguments; $argumentValueMap = []; foreach ($argumentNodes as $argumentNode) { $argumentValueMap[$argumentNode->name->value] = $argumentNode->value; @@ -196,27 +221,32 @@ public static function getArgumentValuesForMap($fieldDefinition, $argumentValueM $argType = $argumentDefinition->getType(); $argumentValueNode = $argumentValueMap[$name] ?? null; - if (! $argumentValueNode) { - if ($argumentDefinition->defaultValueExists()) { - $coercedValues[$name] = $argumentDefinition->defaultValue; - } elseif ($argType instanceof NonNull) { + if ($argumentValueNode instanceof VariableNode) { + $variableName = $argumentValueNode->name->value; + $hasValue = array_key_exists($variableName, $variableValues ?? []); + $isNull = $hasValue ? $variableValues[$variableName] === null : false; + } else { + $hasValue = $argumentValueNode !== null; + $isNull = $argumentValueNode instanceof NullValueNode; + } + + if (! $hasValue && $argumentDefinition->defaultValueExists()) { + // If no argument was provided where the definition has a default value, + // use the default value. + $coercedValues[$name] = $argumentDefinition->defaultValue; + } elseif ((! $hasValue || $isNull) && ($argType instanceof NonNull)) { + // If no argument or a null value was provided to an argument with a + // non-null type (required), produce a field error. + if ($isNull) { throw new Error( - 'Argument "' . $name . '" of required type ' . - '"' . Utils::printSafe($argType) . '" was not provided.', + 'Argument "' . $name . '" of non-null type ' . + '"' . Utils::printSafe($argType) . '" must not be null.', $referenceNode ); } - } elseif ($argumentValueNode instanceof VariableNode) { - $variableName = $argumentValueNode->name->value; - if ($variableValues && array_key_exists($variableName, $variableValues)) { - // Note: this does not check that this variable value is correct. - // This assumes that this query has been validated and the variable - // usage here is of the correct type. - $coercedValues[$name] = $variableValues[$variableName]; - } elseif ($argumentDefinition->defaultValueExists()) { - $coercedValues[$name] = $argumentDefinition->defaultValue; - } elseif ($argType instanceof NonNull) { + if ($argumentValueNode instanceof VariableNode) { + $variableName = $argumentValueNode->name->value; throw new Error( 'Argument "' . $name . '" of required type "' . Utils::printSafe($argType) . '" was ' . 'provided the variable "$' . $variableName . '" which was not provided ' . @@ -224,19 +254,38 @@ public static function getArgumentValuesForMap($fieldDefinition, $argumentValueM [$argumentValueNode] ); } - } else { - $valueNode = $argumentValueNode; - $coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues); - if (Utils::isInvalid($coercedValue)) { - // Note: ValuesOfCorrectType validation should catch this before - // execution. This is a runtime check to ensure execution does not - // continue with an invalid argument value. - throw new Error( - 'Argument "' . $name . '" has invalid value ' . Printer::doPrint($valueNode) . '.', - [$argumentValueNode] - ); + + throw new Error( + 'Argument "' . $name . '" of required type ' . + '"' . Utils::printSafe($argType) . '" was not provided.', + $referenceNode + ); + } elseif ($hasValue) { + if ($argumentValueNode instanceof NullValueNode) { + // If the explicit value `null` was provided, an entry in the coerced + // values must exist as the value `null`. + $coercedValues[$name] = null; + } elseif ($argumentValueNode instanceof VariableNode) { + $variableName = $argumentValueNode->name->value; + Utils::invariant($variableValues !== null, 'Must exist for hasValue to be true.'); + // Note: This does no further checking that this variable is correct. + // This assumes that this query has been validated and the variable + // usage here is of the correct type. + $coercedValues[$name] = $variableValues[$variableName] ?? null; + } else { + $valueNode = $argumentValueNode; + $coercedValue = AST::valueFromAST($valueNode, $argType, $variableValues); + if (Utils::isInvalid($coercedValue)) { + // Note: ValuesOfCorrectType validation should catch this before + // execution. This is a runtime check to ensure execution does not + // continue with an invalid argument value. + throw new Error( + 'Argument "' . $name . '" has invalid value ' . Printer::doPrint($valueNode) . '.', + [$argumentValueNode] + ); + } + $coercedValues[$name] = $coercedValue; } - $coercedValues[$name] = $coercedValue; } } @@ -246,12 +295,15 @@ public static function getArgumentValuesForMap($fieldDefinition, $argumentValueM /** * @deprecated as of 8.0 (Moved to \GraphQL\Utils\AST::valueFromAST) * - * @param ValueNode $valueNode - * @param mixed[]|null $variables + * @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode $valueNode + * @param ScalarType|EnumType|InputObjectType|ListOfType|NonNull $type + * @param mixed[]|null $variables * * @return mixed[]|stdClass|null + * + * @codeCoverageIgnore */ - public static function valueFromAST($valueNode, InputType $type, ?array $variables = null) + public static function valueFromAST(ValueNode $valueNode, InputType $type, ?array $variables = null) { return AST::valueFromAST($valueNode, $type, $variables); } @@ -259,9 +311,12 @@ public static function valueFromAST($valueNode, InputType $type, ?array $variabl /** * @deprecated as of 0.12 (Use coerceValue() directly for richer information) * - * @param mixed[] $value + * @param mixed[] $value + * @param ScalarType|EnumType|InputObjectType|ListOfType|NonNull $type * * @return string[] + * + * @codeCoverageIgnore */ public static function isValidPHPValue($value, InputType $type) { @@ -269,10 +324,11 @@ public static function isValidPHPValue($value, InputType $type) return $errors ? array_map( - static function (Throwable $error) { + static function (Throwable $error) : string { return $error->getMessage(); }, $errors - ) : []; + ) + : []; } } diff --git a/src/Experimental/Executor/Collector.php b/src/Experimental/Executor/Collector.php index 99131badf..dc1b49f73 100644 --- a/src/Experimental/Executor/Collector.php +++ b/src/Experimental/Executor/Collector.php @@ -6,23 +6,31 @@ use Generator; use GraphQL\Error\Error; +use GraphQL\Language\AST\BooleanValueNode; use GraphQL\Language\AST\DefinitionNode; use GraphQL\Language\AST\DocumentNode; +use GraphQL\Language\AST\EnumValueNode; use GraphQL\Language\AST\FieldNode; +use GraphQL\Language\AST\FloatValueNode; use GraphQL\Language\AST\FragmentDefinitionNode; use GraphQL\Language\AST\FragmentSpreadNode; use GraphQL\Language\AST\InlineFragmentNode; +use GraphQL\Language\AST\IntValueNode; +use GraphQL\Language\AST\ListValueNode; use GraphQL\Language\AST\Node; -use GraphQL\Language\AST\NodeKind; +use GraphQL\Language\AST\NullValueNode; +use GraphQL\Language\AST\ObjectValueNode; use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\SelectionSetNode; -use GraphQL\Language\AST\ValueNode; +use GraphQL\Language\AST\StringValueNode; +use GraphQL\Language\AST\VariableNode; use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Introspection; use GraphQL\Type\Schema; +use function count; use function sprintf; /** @@ -48,7 +56,7 @@ class Collector /** @var FieldNode[][] */ private $fields; - /** @var string[] */ + /** @var array */ private $visitedFragments; public function __construct(Schema $schema, Runtime $runtime) @@ -64,8 +72,7 @@ public function initialize(DocumentNode $documentNode, ?string $operationName = foreach ($documentNode->definitions as $definitionNode) { /** @var DefinitionNode|Node $definitionNode */ - if ($definitionNode->kind === NodeKind::OPERATION_DEFINITION) { - /** @var OperationDefinitionNode $definitionNode */ + if ($definitionNode instanceof OperationDefinitionNode) { if ($operationName === null && $this->operation !== null) { $hasMultipleAssumedOperations = true; } @@ -74,8 +81,7 @@ public function initialize(DocumentNode $documentNode, ?string $operationName = ) { $this->operation = $definitionNode; } - } elseif ($definitionNode->kind === NodeKind::FRAGMENT_DEFINITION) { - /** @var FragmentDefinitionNode $definitionNode */ + } elseif ($definitionNode instanceof FragmentDefinitionNode) { $this->fragments[$definitionNode->name->value] = $definitionNode; } } @@ -122,7 +128,7 @@ public function collectFields(ObjectType $runtimeType, ?SelectionSetNode $select $fieldName = $fieldNode->name->value; $argumentValueMap = null; - if (! empty($fieldNode->arguments)) { + if (count($fieldNode->arguments) > 0) { foreach ($fieldNode->arguments as $argumentNode) { $argumentValueMap = $argumentValueMap ?? []; $argumentValueMap[$argumentNode->name->value] = $argumentNode->value; @@ -149,11 +155,10 @@ private function doCollectFields(ObjectType $runtimeType, ?SelectionSetNode $sel foreach ($selectionSet->selections as $selection) { /** @var FieldNode|FragmentSpreadNode|InlineFragmentNode $selection */ - - if (! empty($selection->directives)) { + if (count($selection->directives) > 0) { foreach ($selection->directives as $directiveNode) { if ($directiveNode->name->value === Directive::SKIP_NAME) { - /** @var ValueNode|null $condition */ + /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null $condition */ $condition = null; foreach ($directiveNode->arguments as $argumentNode) { if ($argumentNode->name->value === Directive::IF_ARGUMENT_NAME) { @@ -173,7 +178,7 @@ private function doCollectFields(ObjectType $runtimeType, ?SelectionSetNode $sel } } } elseif ($directiveNode->name->value === Directive::INCLUDE_NAME) { - /** @var ValueNode|null $condition */ + /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null $condition */ $condition = null; foreach ($directiveNode->arguments as $argumentNode) { if ($argumentNode->name->value === Directive::IF_ARGUMENT_NAME) { @@ -196,19 +201,15 @@ private function doCollectFields(ObjectType $runtimeType, ?SelectionSetNode $sel } } - if ($selection->kind === NodeKind::FIELD) { - /** @var FieldNode $selection */ - - $resultName = $selection->alias ? $selection->alias->value : $selection->name->value; + if ($selection instanceof FieldNode) { + $resultName = $selection->alias === null ? $selection->name->value : $selection->alias->value; if (! isset($this->fields[$resultName])) { $this->fields[$resultName] = []; } $this->fields[$resultName][] = $selection; - } elseif ($selection->kind === NodeKind::FRAGMENT_SPREAD) { - /** @var FragmentSpreadNode $selection */ - + } elseif ($selection instanceof FragmentSpreadNode) { $fragmentName = $selection->name->value; if (isset($this->visitedFragments[$fragmentName])) { @@ -243,15 +244,13 @@ private function doCollectFields(ObjectType $runtimeType, ?SelectionSetNode $sel continue; } } elseif ($conditionType instanceof AbstractType) { - if (! $this->schema->isPossibleType($conditionType, $runtimeType)) { + if (! $this->schema->isSubType($conditionType, $runtimeType)) { continue; } } $this->doCollectFields($runtimeType, $fragmentDefinition->selectionSet); - } elseif ($selection->kind === NodeKind::INLINE_FRAGMENT) { - /** @var InlineFragmentNode $selection */ - + } elseif ($selection instanceof InlineFragmentNode) { if ($selection->typeCondition !== null) { $conditionTypeName = $selection->typeCondition->name->value; @@ -270,7 +269,7 @@ private function doCollectFields(ObjectType $runtimeType, ?SelectionSetNode $sel continue; } } elseif ($conditionType instanceof AbstractType) { - if (! $this->schema->isPossibleType($conditionType, $runtimeType)) { + if (! $this->schema->isSubType($conditionType, $runtimeType)) { continue; } } diff --git a/src/Experimental/Executor/CoroutineContext.php b/src/Experimental/Executor/CoroutineContext.php index 910b41d1d..d22ec774e 100644 --- a/src/Experimental/Executor/CoroutineContext.php +++ b/src/Experimental/Executor/CoroutineContext.php @@ -27,7 +27,7 @@ class CoroutineContext /** @var string[] */ public $path; - /** @var ResolveInfo|null */ + /** @var ResolveInfo */ public $resolveInfo; /** @var string[]|null */ diff --git a/src/Experimental/Executor/CoroutineExecutor.php b/src/Experimental/Executor/CoroutineExecutor.php index 91091b40b..749810052 100644 --- a/src/Experimental/Executor/CoroutineExecutor.php +++ b/src/Experimental/Executor/CoroutineExecutor.php @@ -18,6 +18,8 @@ use GraphQL\Language\AST\ValueNode; use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\CompositeType; +use GraphQL\Type\Definition\EnumType; +use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\LeafType; @@ -25,6 +27,7 @@ use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; +use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Introspection; @@ -34,6 +37,7 @@ use SplQueue; use stdClass; use Throwable; +use function count; use function is_array; use function is_string; use function sprintf; @@ -70,10 +74,10 @@ class CoroutineExecutor implements Runtime, ExecutorImplementation /** @var string|null */ private $operationName; - /** @var Collector */ + /** @var Collector|null */ private $collector; - /** @var Error[] */ + /** @var array */ private $errors; /** @var SplQueue */ @@ -82,10 +86,10 @@ class CoroutineExecutor implements Runtime, ExecutorImplementation /** @var SplQueue */ private $schedule; - /** @var stdClass */ + /** @var stdClass|null */ private $rootResult; - /** @var int */ + /** @var int|null */ private $pending; /** @var callable */ @@ -105,6 +109,9 @@ public function __construct( self::$undefined = Utils::undefined(); } + $this->errors = []; + $this->queue = new SplQueue(); + $this->schedule = new SplQueue(); $this->schema = $schema; $this->fieldResolver = $fieldResolver; $this->promiseAdapter = $promiseAdapter; @@ -140,11 +147,12 @@ public static function create( private static function resultToArray($value, $emptyObjectAsStdClass = true) { if ($value instanceof stdClass) { - $array = []; - foreach ($value as $propertyName => $propertyValue) { + $array = (array) $value; + foreach ($array as $propertyName => $propertyValue) { $array[$propertyName] = self::resultToArray($propertyValue); } - if ($emptyObjectAsStdClass && empty($array)) { + + if ($emptyObjectAsStdClass && count($array) === 0) { return new stdClass(); } @@ -174,17 +182,17 @@ public function doExecute() : Promise $this->collector = new Collector($this->schema, $this); $this->collector->initialize($this->documentNode, $this->operationName); - if (! empty($this->errors)) { + if (count($this->errors) > 0) { return $this->promiseAdapter->createFulfilled($this->finishExecute(null, $this->errors)); } [$errors, $coercedVariableValues] = Values::getVariableValues( $this->schema, - $this->collector->operation->variableDefinitions ?: [], - $this->rawVariableValues ?: [] + $this->collector->operation->variableDefinitions ?? [], + $this->rawVariableValues ?? [] ); - if (! empty($errors)) { + if (count($errors ?? []) > 0) { return $this->promiseAdapter->createFulfilled($this->finishExecute(null, $errors)); } @@ -219,7 +227,7 @@ public function doExecute() : Promise $this->run(); if ($this->pending > 0) { - return $this->promiseAdapter->create(function (callable $resolve) { + return $this->promiseAdapter->create(function (callable $resolve) : void { $this->doResolve = $resolve; }); } @@ -234,9 +242,9 @@ public function doExecute() : Promise private function finishExecute($value, array $errors) : ExecutionResult { $this->rootResult = null; - $this->errors = null; - $this->queue = null; - $this->schedule = null; + $this->errors = []; + $this->queue = new SplQueue(); + $this->schedule = new SplQueue(); $this->pending = null; $this->collector = null; $this->variableValues = null; @@ -250,6 +258,8 @@ private function finishExecute($value, array $errors) : ExecutionResult /** * @internal + * + * @param ScalarType|EnumType|InputObjectType|ListOfType|NonNull $type */ public function evaluate(ValueNode $valueNode, InputType $type) { @@ -304,13 +314,13 @@ private function run() $this->promiseAdapter ->then( $value, - function ($value) use ($strand) { + function ($value) use ($strand) : void { $strand->success = true; $strand->value = $value; $this->queue->enqueue($strand); $this->done(); }, - function (Throwable $throwable) use ($strand) { + function (Throwable $throwable) use ($strand) : void { $strand->success = false; $strand->value = $throwable; $this->queue->enqueue($strand); @@ -396,9 +406,8 @@ private function spawn(CoroutineContext $ctx) $returnType = $fieldDefinition->getType(); $ctx->resolveInfo = new ResolveInfo( - $ctx->shared->fieldName, + $fieldDefinition, $ctx->shared->fieldNodes, - $returnType, $ctx->type, $ctx->path, $this->schema, @@ -498,7 +507,7 @@ private function completeValueFast(CoroutineContext $ctx, Type $type, $value, ar if ($type !== $this->schema->getType($type->name)) { $hint = ''; - if ($this->schema->getConfig()->typeLoader) { + if ($this->schema->getConfig()->typeLoader !== null) { $hint = sprintf( 'Make sure that type loader returns the same instance as defined in %s.%s', $ctx->type, @@ -543,7 +552,7 @@ private function completeValueFast(CoroutineContext $ctx, Type $type, $value, ar if ($nonNull && $returnValue === null) { $this->addError(Error::createLocatedError( new InvariantViolation(sprintf( - 'Cannot return null for non-nullable field %s.%s.', + 'Cannot return null for non-nullable field "%s.%s".', $ctx->type->name, $ctx->shared->fieldName )), @@ -620,8 +629,9 @@ private function completeValue(CoroutineContext $ctx, Type $type, $value, array foreach ($value as $itemValue) { ++$index; - $itemPath = $path; - $itemPath[] = $index; // !!! use arrays COW semantics + $itemPath = $path; + $itemPath[] = $index; // !!! use arrays COW semantics + $ctx->resolveInfo->path = $itemPath; try { if (! $this->completeValueFast($ctx, $itemType, $itemValue, $itemPath, $itemReturnValue)) { @@ -646,7 +656,7 @@ private function completeValue(CoroutineContext $ctx, Type $type, $value, array } else { if ($type !== $this->schema->getType($type->name)) { $hint = ''; - if ($this->schema->getConfig()->typeLoader) { + if ($this->schema->getConfig()->typeLoader !== null) { $hint = sprintf( 'Make sure that type loader returns the same instance as defined in %s.%s', $ctx->type, @@ -735,7 +745,7 @@ private function completeValue(CoroutineContext $ctx, Type $type, $value, array $returnValue = null; goto CHECKED_RETURN; - } elseif (! $this->schema->isPossibleType($type, $objectType)) { + } elseif (! $this->schema->isSubType($type, $objectType)) { $this->addError(Error::createLocatedError( new InvariantViolation(sprintf( 'Runtime Object type "%s" is not a possible type for "%s".', @@ -820,9 +830,16 @@ private function completeValue(CoroutineContext $ctx, Type $type, $value, array } else { $childContexts = []; - foreach ($this->collector->collectFields($objectType, $ctx->shared->mergedSelectionSet ?? $this->mergeSelectionSets($ctx)) as $childShared) { - /** @var CoroutineContextShared $childShared */ + $fields = []; + if ($this->collector !== null) { + $fields = $this->collector->collectFields( + $objectType, + $ctx->shared->mergedSelectionSet ?? $this->mergeSelectionSets($ctx) + ); + } + /** @var CoroutineContextShared $childShared */ + foreach ($fields as $childShared) { $childPath = $path; $childPath[] = $childShared->resultName; // !!! uses array COW semantics $childCtx = new CoroutineContext( @@ -863,7 +880,7 @@ private function completeValue(CoroutineContext $ctx, Type $type, $value, array if ($nonNull && $returnValue === null) { $this->addError(Error::createLocatedError( new InvariantViolation(sprintf( - 'Cannot return null for non-nullable field %s.%s.', + 'Cannot return null for non-nullable field "%s.%s".', $ctx->type->name, $ctx->shared->fieldName )), @@ -894,6 +911,11 @@ private function mergeSelectionSets(CoroutineContext $ctx) return $ctx->shared->mergedSelectionSet = new SelectionSetNode(['selections' => $selections]); } + /** + * @param InterfaceType|UnionType $abstractType + * + * @return Generator|ObjectType|Type|null + */ private function resolveTypeSlow(CoroutineContext $ctx, $value, AbstractType $abstractType) { if ($value !== null && @@ -904,7 +926,7 @@ private function resolveTypeSlow(CoroutineContext $ctx, $value, AbstractType $ab return $this->schema->getType($value['__typename']); } - if ($abstractType instanceof InterfaceType && $this->schema->getConfig()->typeLoader) { + if ($abstractType instanceof InterfaceType && $this->schema->getConfig()->typeLoader !== null) { Warning::warnOnce( sprintf( 'GraphQL Interface Type `%s` returned `null` from its `resolveType` function ' . @@ -926,7 +948,7 @@ private function resolveTypeSlow(CoroutineContext $ctx, $value, AbstractType $ab $selectedType = null; foreach ($possibleTypes as $type) { $typeCheck = yield $type->isTypeOf($value, $this->contextValue, $ctx->resolveInfo); - if ($selectedType !== null || $typeCheck !== true) { + if ($selectedType !== null || ! $typeCheck) { continue; } diff --git a/src/Experimental/Executor/Runtime.php b/src/Experimental/Executor/Runtime.php index f8dc14ad8..a5c8fa6d7 100644 --- a/src/Experimental/Executor/Runtime.php +++ b/src/Experimental/Executor/Runtime.php @@ -5,13 +5,21 @@ namespace GraphQL\Experimental\Executor; use GraphQL\Language\AST\ValueNode; +use GraphQL\Type\Definition\EnumType; +use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputType; +use GraphQL\Type\Definition\ListOfType; +use GraphQL\Type\Definition\NonNull; +use GraphQL\Type\Definition\ScalarType; /** * @internal */ interface Runtime { + /** + * @param ScalarType|EnumType|InputObjectType|ListOfType|NonNull $type + */ public function evaluate(ValueNode $valueNode, InputType $type); public function addError($error); diff --git a/src/GraphQL.php b/src/GraphQL.php index a9819be80..8f5291c6f 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -16,12 +16,14 @@ use GraphQL\Language\Parser; use GraphQL\Language\Source; use GraphQL\Type\Definition\Directive; +use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema as SchemaType; use GraphQL\Validator\DocumentValidator; use GraphQL\Validator\Rules\QueryComplexity; use GraphQL\Validator\Rules\ValidationRule; use function array_values; +use function count; use function trigger_error; use const E_USER_DEPRECATED; @@ -47,9 +49,11 @@ class GraphQL * rootValue: * The value provided as the first argument to resolver functions on the top * level type (e.g. the query object type). - * context: - * The value provided as the third argument to all resolvers. - * Use this to pass current session, user data, etc + * contextValue: + * The context value is provided as an argument to resolver functions after + * field arguments. It is used to pass shared information useful at any point + * during executing this query, for example the currently logged in user and + * connections to databases or other services. * variableValues: * A mapping of variable name to runtime value to use for all variables * defined in the requestString. @@ -68,7 +72,7 @@ class GraphQL * * @param string|DocumentNode $source * @param mixed $rootValue - * @param mixed $context + * @param mixed $contextValue * @param mixed[]|null $variableValues * @param ValidationRule[] $validationRules * @@ -78,7 +82,7 @@ public static function executeQuery( SchemaType $schema, $source, $rootValue = null, - $context = null, + $contextValue = null, $variableValues = null, ?string $operationName = null, ?callable $fieldResolver = null, @@ -91,7 +95,7 @@ public static function executeQuery( $schema, $source, $rootValue, - $context, + $contextValue, $variableValues, $operationName, $fieldResolver, @@ -128,11 +132,11 @@ public static function promiseToExecute( if ($source instanceof DocumentNode) { $documentNode = $source; } else { - $documentNode = Parser::parse(new Source($source ?: '', 'GraphQL')); + $documentNode = Parser::parse(new Source($source ?? '', 'GraphQL')); } // FIXME - if (empty($validationRules)) { + if (count($validationRules ?? []) === 0) { /** @var QueryComplexity $queryComplexity */ $queryComplexity = DocumentValidator::getRule(QueryComplexity::class); $queryComplexity->setRawVariableValues($variableValues); @@ -148,7 +152,7 @@ public static function promiseToExecute( $validationErrors = DocumentValidator::validate($schema, $documentNode, $validationRules); - if (! empty($validationErrors)) { + if (count($validationErrors) > 0) { return $promiseAdapter->createFulfilled( new ExecutionResult(null, $validationErrors) ); @@ -180,6 +184,8 @@ public static function promiseToExecute( * @param mixed[]|null $variableValues * * @return Promise|mixed[] + * + * @codeCoverageIgnore */ public static function execute( SchemaType $schema, @@ -208,7 +214,7 @@ public static function execute( if ($promiseAdapter instanceof SyncPromiseAdapter) { $result = $promiseAdapter->wait($result)->toArray(); } else { - $result = $result->then(static function (ExecutionResult $r) { + $result = $result->then(static function (ExecutionResult $r) : array { return $r->toArray(); }); } @@ -225,6 +231,8 @@ public static function execute( * @param mixed[]|null $variableValues * * @return ExecutionResult|Promise + * + * @codeCoverageIgnore */ public static function executeAndReturnResult( SchemaType $schema, @@ -285,7 +293,7 @@ public static function getStandardTypes() : array * Replaces standard types with types from this list (matching by name) * Standard types not listed here remain untouched. * - * @param Type[] $types + * @param array $types * * @api */ @@ -326,6 +334,10 @@ public static function setPromiseAdapter(?PromiseAdapter $promiseAdapter = null) */ public static function useExperimentalExecutor() { + trigger_error( + 'Experimental Executor is deprecated and will be removed in the next major version', + E_USER_DEPRECATED + ); Executor::setImplementationFactory([CoroutineExecutor::class, 'create']); } @@ -343,6 +355,8 @@ public static function useReferenceExecutor() * @deprecated Renamed to getStandardDirectives * * @return Directive[] + * + * @codeCoverageIgnore */ public static function getInternalDirectives() : array { diff --git a/src/Language/AST/ArgumentNode.php b/src/Language/AST/ArgumentNode.php index 545a855ce..0abfb6fdc 100644 --- a/src/Language/AST/ArgumentNode.php +++ b/src/Language/AST/ArgumentNode.php @@ -9,7 +9,7 @@ class ArgumentNode extends Node /** @var string */ public $kind = NodeKind::ARGUMENT; - /** @var ValueNode */ + /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode */ public $value; /** @var NameNode */ diff --git a/src/Language/AST/DefinitionNode.php b/src/Language/AST/DefinitionNode.php index b3255d42d..c331e2342 100644 --- a/src/Language/AST/DefinitionNode.php +++ b/src/Language/AST/DefinitionNode.php @@ -7,7 +7,7 @@ /** * export type DefinitionNode = * | ExecutableDefinitionNode - * | TypeSystemDefinitionNode; // experimental non-spec addition. + * | TypeSystemDefinitionNode; */ interface DefinitionNode { diff --git a/src/Language/AST/DirectiveDefinitionNode.php b/src/Language/AST/DirectiveDefinitionNode.php index 825c7f789..ed75ba25a 100644 --- a/src/Language/AST/DirectiveDefinitionNode.php +++ b/src/Language/AST/DirectiveDefinitionNode.php @@ -12,12 +12,15 @@ class DirectiveDefinitionNode extends Node implements TypeSystemDefinitionNode /** @var NameNode */ public $name; - /** @var ArgumentNode[] */ + /** @var StringValueNode|null */ + public $description; + + /** @var NodeList */ public $arguments; - /** @var NameNode[] */ - public $locations; + /** @var bool */ + public $repeatable; - /** @var StringValueNode|null */ - public $description; + /** @var NodeList */ + public $locations; } diff --git a/src/Language/AST/DirectiveNode.php b/src/Language/AST/DirectiveNode.php index 09de36634..c75aafb29 100644 --- a/src/Language/AST/DirectiveNode.php +++ b/src/Language/AST/DirectiveNode.php @@ -12,6 +12,6 @@ class DirectiveNode extends Node /** @var NameNode */ public $name; - /** @var ArgumentNode[] */ + /** @var NodeList */ public $arguments; } diff --git a/src/Language/AST/DocumentNode.php b/src/Language/AST/DocumentNode.php index bcf051bd4..ad421aa21 100644 --- a/src/Language/AST/DocumentNode.php +++ b/src/Language/AST/DocumentNode.php @@ -9,6 +9,6 @@ class DocumentNode extends Node /** @var string */ public $kind = NodeKind::DOCUMENT; - /** @var NodeList|DefinitionNode[] */ + /** @var NodeList */ public $definitions; } diff --git a/src/Language/AST/EnumTypeDefinitionNode.php b/src/Language/AST/EnumTypeDefinitionNode.php index 4ecd8fd56..5d8c208ed 100644 --- a/src/Language/AST/EnumTypeDefinitionNode.php +++ b/src/Language/AST/EnumTypeDefinitionNode.php @@ -12,10 +12,10 @@ class EnumTypeDefinitionNode extends Node implements TypeDefinitionNode /** @var NameNode */ public $name; - /** @var DirectiveNode[] */ + /** @var NodeList */ public $directives; - /** @var EnumValueDefinitionNode[]|NodeList|null */ + /** @var NodeList */ public $values; /** @var StringValueNode|null */ diff --git a/src/Language/AST/EnumTypeExtensionNode.php b/src/Language/AST/EnumTypeExtensionNode.php index 7812abc91..2c90fef63 100644 --- a/src/Language/AST/EnumTypeExtensionNode.php +++ b/src/Language/AST/EnumTypeExtensionNode.php @@ -12,9 +12,9 @@ class EnumTypeExtensionNode extends Node implements TypeExtensionNode /** @var NameNode */ public $name; - /** @var DirectiveNode[]|null */ + /** @var NodeList */ public $directives; - /** @var EnumValueDefinitionNode[]|null */ + /** @var NodeList */ public $values; } diff --git a/src/Language/AST/EnumValueDefinitionNode.php b/src/Language/AST/EnumValueDefinitionNode.php index 1eff6c2bb..0f9aa8d1a 100644 --- a/src/Language/AST/EnumValueDefinitionNode.php +++ b/src/Language/AST/EnumValueDefinitionNode.php @@ -12,7 +12,7 @@ class EnumValueDefinitionNode extends Node /** @var NameNode */ public $name; - /** @var DirectiveNode[] */ + /** @var NodeList */ public $directives; /** @var StringValueNode|null */ diff --git a/src/Language/AST/FieldDefinitionNode.php b/src/Language/AST/FieldDefinitionNode.php index 9e48f1980..9b9f20155 100644 --- a/src/Language/AST/FieldDefinitionNode.php +++ b/src/Language/AST/FieldDefinitionNode.php @@ -12,13 +12,13 @@ class FieldDefinitionNode extends Node /** @var NameNode */ public $name; - /** @var InputValueDefinitionNode[]|NodeList */ + /** @var NodeList */ public $arguments; - /** @var TypeNode */ + /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */ public $type; - /** @var DirectiveNode[]|NodeList */ + /** @var NodeList */ public $directives; /** @var StringValueNode|null */ diff --git a/src/Language/AST/FieldNode.php b/src/Language/AST/FieldNode.php index e3c4125b9..174dbd979 100644 --- a/src/Language/AST/FieldNode.php +++ b/src/Language/AST/FieldNode.php @@ -15,10 +15,10 @@ class FieldNode extends Node implements SelectionNode /** @var NameNode|null */ public $alias; - /** @var ArgumentNode[]|null */ + /** @var NodeList */ public $arguments; - /** @var DirectiveNode[]|null */ + /** @var NodeList */ public $directives; /** @var SelectionSetNode|null */ diff --git a/src/Language/AST/FragmentDefinitionNode.php b/src/Language/AST/FragmentDefinitionNode.php index 5c53e4a40..5cef904c2 100644 --- a/src/Language/AST/FragmentDefinitionNode.php +++ b/src/Language/AST/FragmentDefinitionNode.php @@ -16,14 +16,14 @@ class FragmentDefinitionNode extends Node implements ExecutableDefinitionNode, H * Note: fragment variable definitions are experimental and may be changed * or removed in the future. * - * @var VariableDefinitionNode[]|NodeList + * @var NodeList */ public $variableDefinitions; /** @var NamedTypeNode */ public $typeCondition; - /** @var DirectiveNode[]|NodeList */ + /** @var NodeList */ public $directives; /** @var SelectionSetNode */ diff --git a/src/Language/AST/FragmentSpreadNode.php b/src/Language/AST/FragmentSpreadNode.php index f6ca72c3d..2dc693b27 100644 --- a/src/Language/AST/FragmentSpreadNode.php +++ b/src/Language/AST/FragmentSpreadNode.php @@ -12,6 +12,6 @@ class FragmentSpreadNode extends Node implements SelectionNode /** @var NameNode */ public $name; - /** @var DirectiveNode[] */ + /** @var NodeList */ public $directives; } diff --git a/src/Language/AST/HasSelectionSet.php b/src/Language/AST/HasSelectionSet.php index a80bcaee0..a412cf7d4 100644 --- a/src/Language/AST/HasSelectionSet.php +++ b/src/Language/AST/HasSelectionSet.php @@ -4,10 +4,12 @@ namespace GraphQL\Language\AST; +/** + * export type DefinitionNode = OperationDefinitionNode + * | FragmentDefinitionNode + * + * @property SelectionSetNode $selectionSet + */ interface HasSelectionSet { - /** - * export type DefinitionNode = OperationDefinitionNode - * | FragmentDefinitionNode - */ } diff --git a/src/Language/AST/InlineFragmentNode.php b/src/Language/AST/InlineFragmentNode.php index fe2200814..0c12e22f3 100644 --- a/src/Language/AST/InlineFragmentNode.php +++ b/src/Language/AST/InlineFragmentNode.php @@ -12,7 +12,7 @@ class InlineFragmentNode extends Node implements SelectionNode /** @var NamedTypeNode */ public $typeCondition; - /** @var DirectiveNode[]|null */ + /** @var NodeList */ public $directives; /** @var SelectionSetNode */ diff --git a/src/Language/AST/InputObjectTypeDefinitionNode.php b/src/Language/AST/InputObjectTypeDefinitionNode.php index f761e1f23..cc3245451 100644 --- a/src/Language/AST/InputObjectTypeDefinitionNode.php +++ b/src/Language/AST/InputObjectTypeDefinitionNode.php @@ -12,10 +12,10 @@ class InputObjectTypeDefinitionNode extends Node implements TypeDefinitionNode /** @var NameNode */ public $name; - /** @var DirectiveNode[]|null */ + /** @var NodeList */ public $directives; - /** @var InputValueDefinitionNode[]|null */ + /** @var NodeList */ public $fields; /** @var StringValueNode|null */ diff --git a/src/Language/AST/InputObjectTypeExtensionNode.php b/src/Language/AST/InputObjectTypeExtensionNode.php index b9ee90b68..e470958e5 100644 --- a/src/Language/AST/InputObjectTypeExtensionNode.php +++ b/src/Language/AST/InputObjectTypeExtensionNode.php @@ -12,9 +12,9 @@ class InputObjectTypeExtensionNode extends Node implements TypeExtensionNode /** @var NameNode */ public $name; - /** @var DirectiveNode[]|null */ + /** @var NodeList */ public $directives; - /** @var InputValueDefinitionNode[]|null */ + /** @var NodeList */ public $fields; } diff --git a/src/Language/AST/InputValueDefinitionNode.php b/src/Language/AST/InputValueDefinitionNode.php index afbaabbfa..3e5a4aa5d 100644 --- a/src/Language/AST/InputValueDefinitionNode.php +++ b/src/Language/AST/InputValueDefinitionNode.php @@ -12,13 +12,13 @@ class InputValueDefinitionNode extends Node /** @var NameNode */ public $name; - /** @var TypeNode */ + /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */ public $type; - /** @var ValueNode */ + /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null */ public $defaultValue; - /** @var DirectiveNode[] */ + /** @var NodeList */ public $directives; /** @var StringValueNode|null */ diff --git a/src/Language/AST/InterfaceTypeDefinitionNode.php b/src/Language/AST/InterfaceTypeDefinitionNode.php index b14bd7a1e..d0cae0eba 100644 --- a/src/Language/AST/InterfaceTypeDefinitionNode.php +++ b/src/Language/AST/InterfaceTypeDefinitionNode.php @@ -12,10 +12,13 @@ class InterfaceTypeDefinitionNode extends Node implements TypeDefinitionNode /** @var NameNode */ public $name; - /** @var DirectiveNode[]|null */ + /** @var NodeList */ public $directives; - /** @var FieldDefinitionNode[]|null */ + /** @var NodeList */ + public $interfaces; + + /** @var NodeList */ public $fields; /** @var StringValueNode|null */ diff --git a/src/Language/AST/InterfaceTypeExtensionNode.php b/src/Language/AST/InterfaceTypeExtensionNode.php index 9528f3d2a..4b30f3dad 100644 --- a/src/Language/AST/InterfaceTypeExtensionNode.php +++ b/src/Language/AST/InterfaceTypeExtensionNode.php @@ -12,9 +12,12 @@ class InterfaceTypeExtensionNode extends Node implements TypeExtensionNode /** @var NameNode */ public $name; - /** @var DirectiveNode[]|null */ + /** @var NodeList */ public $directives; - /** @var FieldDefinitionNode[]|null */ + /** @var NodeList */ + public $interfaces; + + /** @var NodeList */ public $fields; } diff --git a/src/Language/AST/ListTypeNode.php b/src/Language/AST/ListTypeNode.php index 104810a75..6bb3902b4 100644 --- a/src/Language/AST/ListTypeNode.php +++ b/src/Language/AST/ListTypeNode.php @@ -9,6 +9,6 @@ class ListTypeNode extends Node implements TypeNode /** @var string */ public $kind = NodeKind::LIST_TYPE; - /** @var Node */ + /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */ public $type; } diff --git a/src/Language/AST/ListValueNode.php b/src/Language/AST/ListValueNode.php index 3720c85a7..78792c942 100644 --- a/src/Language/AST/ListValueNode.php +++ b/src/Language/AST/ListValueNode.php @@ -9,6 +9,6 @@ class ListValueNode extends Node implements ValueNode /** @var string */ public $kind = NodeKind::LST; - /** @var ValueNode[]|NodeList */ + /** @var NodeList */ public $values; } diff --git a/src/Language/AST/Location.php b/src/Language/AST/Location.php index 72135fc20..dfd70b68b 100644 --- a/src/Language/AST/Location.php +++ b/src/Language/AST/Location.php @@ -30,14 +30,14 @@ class Location /** * The Token at which this Node begins. * - * @var Token + * @var Token|null */ public $startToken; /** * The Token at which this Node ends. * - * @var Token + * @var Token|null */ public $endToken; @@ -69,7 +69,7 @@ public function __construct(?Token $startToken = null, ?Token $endToken = null, $this->endToken = $endToken; $this->source = $source; - if (! $startToken || ! $endToken) { + if ($startToken === null || $endToken === null) { return; } diff --git a/src/Language/AST/Node.php b/src/Language/AST/Node.php index 2205d6d5f..3f2688db1 100644 --- a/src/Language/AST/Node.php +++ b/src/Language/AST/Node.php @@ -5,6 +5,7 @@ namespace GraphQL\Language\AST; use GraphQL\Utils\Utils; +use function count; use function get_object_vars; use function is_array; use function is_scalar; @@ -36,15 +37,18 @@ */ abstract class Node { - /** @var Location */ + /** @var Location|null */ public $loc; + /** @var string */ + public $kind; + /** * @param (NameNode|NodeList|SelectionSetNode|Location|string|int|bool|float|null)[] $vars */ public function __construct(array $vars) { - if (empty($vars)) { + if (count($vars) === 0) { return; } @@ -83,10 +87,7 @@ private function cloneValue($value) return $cloned; } - /** - * @return string - */ - public function __toString() + public function __toString() : string { $tmp = $this->toArray(true); @@ -94,11 +95,9 @@ public function __toString() } /** - * @param bool $recursive - * * @return mixed[] */ - public function toArray($recursive = false) + public function toArray(bool $recursive = false) : array { if ($recursive) { return $this->recursiveToArray($this); @@ -106,7 +105,7 @@ public function toArray($recursive = false) $tmp = (array) $this; - if ($this->loc) { + if ($this->loc !== null) { $tmp['loc'] = [ 'start' => $this->loc->start, 'end' => $this->loc->end, @@ -125,7 +124,7 @@ private function recursiveToArray(Node $node) 'kind' => $node->kind, ]; - if ($node->loc) { + if ($node->loc !== null) { $result['loc'] = [ 'start' => $node->loc->start, 'end' => $node->loc->end, diff --git a/src/Language/AST/NodeList.php b/src/Language/AST/NodeList.php index 648f68a71..c94c40c5f 100644 --- a/src/Language/AST/NodeList.php +++ b/src/Language/AST/NodeList.php @@ -6,31 +6,43 @@ use ArrayAccess; use Countable; -use Generator; use GraphQL\Utils\AST; +use InvalidArgumentException; use IteratorAggregate; +use Traversable; use function array_merge; use function array_splice; use function count; use function is_array; +/** + * @template T of Node + * @phpstan-implements ArrayAccess + * @phpstan-implements IteratorAggregate + */ class NodeList implements ArrayAccess, IteratorAggregate, Countable { - /** @var Node[]|mixed[] */ + /** + * @var Node[] + * @phpstan-var array + */ private $nodes; /** - * @param Node[]|mixed[] $nodes + * @param Node[] $nodes * - * @return static + * @phpstan-param array $nodes + * @phpstan-return self */ - public static function create(array $nodes) + public static function create(array $nodes) : self { return new static($nodes); } /** - * @param Node[]|mixed[] $nodes + * @param Node[] $nodes + * + * @phpstan-param array $nodes */ public function __construct(array $nodes) { @@ -38,59 +50,75 @@ public function __construct(array $nodes) } /** - * @param mixed $offset - * - * @return bool + * @param int|string $offset */ - public function offsetExists($offset) + public function offsetExists($offset) : bool { return isset($this->nodes[$offset]); } /** - * @param mixed $offset + * TODO enable strict typing by changing how the Visitor deals with NodeList. + * Ideally, this function should always return a Node instance. + * However, the Visitor currently allows mutation of the NodeList + * and puts arbitrary values in the NodeList, such as strings. + * We will have to switch to using an array or a less strict + * type instead so we can enable strict typing in this class. * - * @return mixed + * @param int|string $offset + * + * @phpstan-return T */ - public function offsetGet($offset) + public function offsetGet($offset)// : Node { $item = $this->nodes[$offset]; if (is_array($item) && isset($item['kind'])) { - $this->nodes[$offset] = $item = AST::fromArray($item); + /** @phpstan-var T $node */ + $node = AST::fromArray($item); + $this->nodes[$offset] = $node; } - return $item; + return $this->nodes[$offset]; } /** - * @param mixed $offset - * @param mixed $value + * @param int|string|null $offset + * @param Node|mixed[] $value + * + * @phpstan-param T|mixed[] $value */ - public function offsetSet($offset, $value) + public function offsetSet($offset, $value) : void { - if (is_array($value) && isset($value['kind'])) { + if (is_array($value)) { + /** @phpstan-var T $value */ $value = AST::fromArray($value); } + + // Happens when a Node is pushed via []= + if ($offset === null) { + $this->nodes[] = $value; + + return; + } + $this->nodes[$offset] = $value; } /** - * @param mixed $offset + * @param int|string $offset */ - public function offsetUnset($offset) + public function offsetUnset($offset) : void { unset($this->nodes[$offset]); } /** - * @param int $offset - * @param int $length * @param mixed $replacement * - * @return NodeList + * @phpstan-return NodeList */ - public function splice($offset, $length, $replacement = null) + public function splice(int $offset, int $length, $replacement = null) : NodeList { return new NodeList(array_splice($this->nodes, $offset, $length, $replacement)); } @@ -98,9 +126,10 @@ public function splice($offset, $length, $replacement = null) /** * @param NodeList|Node[] $list * - * @return NodeList + * @phpstan-param NodeList|array $list + * @phpstan-return NodeList */ - public function merge($list) + public function merge($list) : NodeList { if ($list instanceof self) { $list = $list->nodes; @@ -109,20 +138,14 @@ public function merge($list) return new NodeList(array_merge($this->nodes, $list)); } - /** - * @return Generator - */ - public function getIterator() + public function getIterator() : Traversable { foreach ($this->nodes as $key => $_) { yield $this->offsetGet($key); } } - /** - * @return int - */ - public function count() + public function count() : int { return count($this->nodes); } diff --git a/src/Language/AST/NonNullTypeNode.php b/src/Language/AST/NonNullTypeNode.php index f8b97c04c..18dc70e4b 100644 --- a/src/Language/AST/NonNullTypeNode.php +++ b/src/Language/AST/NonNullTypeNode.php @@ -9,6 +9,6 @@ class NonNullTypeNode extends Node implements TypeNode /** @var string */ public $kind = NodeKind::NON_NULL_TYPE; - /** @var NameNode | ListTypeNode */ + /** @var NamedTypeNode|ListTypeNode */ public $type; } diff --git a/src/Language/AST/ObjectFieldNode.php b/src/Language/AST/ObjectFieldNode.php index cd458e8dc..bcced05a3 100644 --- a/src/Language/AST/ObjectFieldNode.php +++ b/src/Language/AST/ObjectFieldNode.php @@ -12,6 +12,6 @@ class ObjectFieldNode extends Node /** @var NameNode */ public $name; - /** @var ValueNode */ + /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode */ public $value; } diff --git a/src/Language/AST/ObjectTypeDefinitionNode.php b/src/Language/AST/ObjectTypeDefinitionNode.php index 0339c3572..d221a101e 100644 --- a/src/Language/AST/ObjectTypeDefinitionNode.php +++ b/src/Language/AST/ObjectTypeDefinitionNode.php @@ -12,13 +12,13 @@ class ObjectTypeDefinitionNode extends Node implements TypeDefinitionNode /** @var NameNode */ public $name; - /** @var NamedTypeNode[] */ - public $interfaces = []; + /** @var NodeList */ + public $interfaces; - /** @var DirectiveNode[]|null */ + /** @var NodeList */ public $directives; - /** @var FieldDefinitionNode[]|null */ + /** @var NodeList */ public $fields; /** @var StringValueNode|null */ diff --git a/src/Language/AST/ObjectTypeExtensionNode.php b/src/Language/AST/ObjectTypeExtensionNode.php index 45224497d..b1375238d 100644 --- a/src/Language/AST/ObjectTypeExtensionNode.php +++ b/src/Language/AST/ObjectTypeExtensionNode.php @@ -12,12 +12,12 @@ class ObjectTypeExtensionNode extends Node implements TypeExtensionNode /** @var NameNode */ public $name; - /** @var NamedTypeNode[] */ - public $interfaces = []; + /** @var NodeList */ + public $interfaces; - /** @var DirectiveNode[] */ + /** @var NodeList */ public $directives; - /** @var FieldDefinitionNode[] */ + /** @var NodeList */ public $fields; } diff --git a/src/Language/AST/ObjectValueNode.php b/src/Language/AST/ObjectValueNode.php index 7e0d71caa..f83d74af7 100644 --- a/src/Language/AST/ObjectValueNode.php +++ b/src/Language/AST/ObjectValueNode.php @@ -9,6 +9,6 @@ class ObjectValueNode extends Node implements ValueNode /** @var string */ public $kind = NodeKind::OBJECT; - /** @var ObjectFieldNode[]|NodeList */ + /** @var NodeList */ public $fields; } diff --git a/src/Language/AST/OperationDefinitionNode.php b/src/Language/AST/OperationDefinitionNode.php index cef005353..cef2abe24 100644 --- a/src/Language/AST/OperationDefinitionNode.php +++ b/src/Language/AST/OperationDefinitionNode.php @@ -9,16 +9,16 @@ class OperationDefinitionNode extends Node implements ExecutableDefinitionNode, /** @var string */ public $kind = NodeKind::OPERATION_DEFINITION; - /** @var NameNode */ + /** @var NameNode|null */ public $name; - /** @var string (oneOf 'query', 'mutation')) */ + /** @var string (oneOf 'query', 'mutation', 'subscription')) */ public $operation; - /** @var VariableDefinitionNode[] */ + /** @var NodeList */ public $variableDefinitions; - /** @var DirectiveNode[] */ + /** @var NodeList */ public $directives; /** @var SelectionSetNode */ diff --git a/src/Language/AST/ScalarTypeDefinitionNode.php b/src/Language/AST/ScalarTypeDefinitionNode.php index 5c906623f..f30a28240 100644 --- a/src/Language/AST/ScalarTypeDefinitionNode.php +++ b/src/Language/AST/ScalarTypeDefinitionNode.php @@ -12,7 +12,7 @@ class ScalarTypeDefinitionNode extends Node implements TypeDefinitionNode /** @var NameNode */ public $name; - /** @var DirectiveNode[] */ + /** @var NodeList */ public $directives; /** @var StringValueNode|null */ diff --git a/src/Language/AST/ScalarTypeExtensionNode.php b/src/Language/AST/ScalarTypeExtensionNode.php index fe8831681..ea7d16a69 100644 --- a/src/Language/AST/ScalarTypeExtensionNode.php +++ b/src/Language/AST/ScalarTypeExtensionNode.php @@ -12,6 +12,6 @@ class ScalarTypeExtensionNode extends Node implements TypeExtensionNode /** @var NameNode */ public $name; - /** @var DirectiveNode[]|null */ + /** @var NodeList */ public $directives; } diff --git a/src/Language/AST/SchemaDefinitionNode.php b/src/Language/AST/SchemaDefinitionNode.php index 20e6a8954..f563ec744 100644 --- a/src/Language/AST/SchemaDefinitionNode.php +++ b/src/Language/AST/SchemaDefinitionNode.php @@ -9,9 +9,9 @@ class SchemaDefinitionNode extends Node implements TypeSystemDefinitionNode /** @var string */ public $kind = NodeKind::SCHEMA_DEFINITION; - /** @var DirectiveNode[] */ + /** @var NodeList */ public $directives; - /** @var OperationTypeDefinitionNode[] */ + /** @var NodeList */ public $operationTypes; } diff --git a/src/Language/AST/SchemaTypeExtensionNode.php b/src/Language/AST/SchemaTypeExtensionNode.php index e86177d97..c96bcdf67 100644 --- a/src/Language/AST/SchemaTypeExtensionNode.php +++ b/src/Language/AST/SchemaTypeExtensionNode.php @@ -9,9 +9,9 @@ class SchemaTypeExtensionNode extends Node implements TypeExtensionNode /** @var string */ public $kind = NodeKind::SCHEMA_EXTENSION; - /** @var DirectiveNode[]|null */ + /** @var NodeList */ public $directives; - /** @var OperationTypeDefinitionNode[]|null */ + /** @var NodeList */ public $operationTypes; } diff --git a/src/Language/AST/SelectionSetNode.php b/src/Language/AST/SelectionSetNode.php index 427674e67..ddae64843 100644 --- a/src/Language/AST/SelectionSetNode.php +++ b/src/Language/AST/SelectionSetNode.php @@ -9,6 +9,6 @@ class SelectionSetNode extends Node /** @var string */ public $kind = NodeKind::SELECTION_SET; - /** @var SelectionNode[] */ + /** @var NodeList */ public $selections; } diff --git a/src/Language/AST/StringValueNode.php b/src/Language/AST/StringValueNode.php index b1fe0dfef..059a7e310 100644 --- a/src/Language/AST/StringValueNode.php +++ b/src/Language/AST/StringValueNode.php @@ -12,6 +12,6 @@ class StringValueNode extends Node implements ValueNode /** @var string */ public $value; - /** @var bool|null */ + /** @var bool */ public $block; } diff --git a/src/Language/AST/TypeSystemDefinitionNode.php b/src/Language/AST/TypeSystemDefinitionNode.php index 65553ab90..4a31f2e28 100644 --- a/src/Language/AST/TypeSystemDefinitionNode.php +++ b/src/Language/AST/TypeSystemDefinitionNode.php @@ -10,6 +10,8 @@ * | TypeDefinitionNode * | TypeExtensionNode * | DirectiveDefinitionNode + * + * @property NameNode $name */ interface TypeSystemDefinitionNode extends DefinitionNode { diff --git a/src/Language/AST/UnionTypeDefinitionNode.php b/src/Language/AST/UnionTypeDefinitionNode.php index 1bfef6993..d725d8efb 100644 --- a/src/Language/AST/UnionTypeDefinitionNode.php +++ b/src/Language/AST/UnionTypeDefinitionNode.php @@ -12,10 +12,10 @@ class UnionTypeDefinitionNode extends Node implements TypeDefinitionNode /** @var NameNode */ public $name; - /** @var DirectiveNode[] */ + /** @var NodeList */ public $directives; - /** @var NamedTypeNode[]|null */ + /** @var NodeList */ public $types; /** @var StringValueNode|null */ diff --git a/src/Language/AST/UnionTypeExtensionNode.php b/src/Language/AST/UnionTypeExtensionNode.php index 53bf3ed40..b10eebd5b 100644 --- a/src/Language/AST/UnionTypeExtensionNode.php +++ b/src/Language/AST/UnionTypeExtensionNode.php @@ -12,9 +12,9 @@ class UnionTypeExtensionNode extends Node implements TypeExtensionNode /** @var NameNode */ public $name; - /** @var DirectiveNode[]|null */ + /** @var NodeList */ public $directives; - /** @var NamedTypeNode[]|null */ + /** @var NodeList */ public $types; } diff --git a/src/Language/AST/VariableDefinitionNode.php b/src/Language/AST/VariableDefinitionNode.php index eb76b666c..1ab87f39c 100644 --- a/src/Language/AST/VariableDefinitionNode.php +++ b/src/Language/AST/VariableDefinitionNode.php @@ -12,9 +12,12 @@ class VariableDefinitionNode extends Node implements DefinitionNode /** @var VariableNode */ public $variable; - /** @var TypeNode */ + /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */ public $type; - /** @var ValueNode|null */ + /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null */ public $defaultValue; + + /** @var NodeList */ + public $directives; } diff --git a/src/Language/DirectiveLocation.php b/src/Language/DirectiveLocation.php index 5f22537c7..3a3273286 100644 --- a/src/Language/DirectiveLocation.php +++ b/src/Language/DirectiveLocation.php @@ -17,6 +17,7 @@ class DirectiveLocation const FRAGMENT_DEFINITION = 'FRAGMENT_DEFINITION'; const FRAGMENT_SPREAD = 'FRAGMENT_SPREAD'; const INLINE_FRAGMENT = 'INLINE_FRAGMENT'; + const VARIABLE_DEFINITION = 'VARIABLE_DEFINITION'; // Type System Definitions const SCHEMA = 'SCHEMA'; @@ -53,12 +54,7 @@ class DirectiveLocation self::INPUT_FIELD_DEFINITION => self::INPUT_FIELD_DEFINITION, ]; - /** - * @param string $name - * - * @return bool - */ - public static function has($name) + public static function has(string $name) : bool { return isset(self::$locations[$name]); } diff --git a/src/Language/Lexer.php b/src/Language/Lexer.php index 3aad4dd20..15563bb23 100644 --- a/src/Language/Lexer.php +++ b/src/Language/Lexer.php @@ -9,8 +9,11 @@ use GraphQL\Utils\Utils; use function chr; use function hexdec; +use function mb_convert_encoding; use function ord; +use function pack; use function preg_match; +use function substr; /** * A Lexer is a stateful stream generator in that every time @@ -118,7 +121,7 @@ public function lookahead() $token = $this->token; if ($token->kind !== Token::EOF) { do { - $token = $token->next ?: ($token->next = $this->readToken($token)); + $token = $token->next ?? ($token->next = $this->readToken($token)); } while ($token->kind === Token::COMMENT); } @@ -314,11 +317,11 @@ private function readName($line, $col, Token $prev) $start = $this->position; [$char, $code] = $this->readChar(); - while ($code && ( + while ($code !== null && ( $code === 95 || // _ - $code >= 48 && $code <= 57 || // 0-9 - $code >= 65 && $code <= 90 || // A-Z - $code >= 97 && $code <= 122 // a-z + ($code >= 48 && $code <= 57) || // 0-9 + ($code >= 65 && $code <= 90) || // A-Z + ($code >= 97 && $code <= 122) // a-z )) { $value .= $char; [$char, $code] = $this->moveStringCursor(1, 1)->readChar(); @@ -522,8 +525,27 @@ private function readString($line, $col, Token $prev) 'Invalid character escape sequence: \\u' . $hex ); } + $code = hexdec($hex); + + // UTF-16 surrogate pair detection and handling. + $highOrderByte = $code >> 8; + if (0xD8 <= $highOrderByte && $highOrderByte <= 0xDF) { + [$utf16Continuation] = $this->readChars(6, true); + if (! preg_match('/^\\\u[0-9a-fA-F]{4}$/', $utf16Continuation)) { + throw new SyntaxError( + $this->source, + $this->position - 5, + 'Invalid UTF-16 trailing surrogate: ' . $utf16Continuation + ); + } + $surrogatePairHex = $hex . substr($utf16Continuation, 2, 4); + $value .= mb_convert_encoding(pack('H*', $surrogatePairHex), 'UTF-8', 'UTF-16'); + break; + } + $this->assertValidStringCharacterCode($code, $position - 2); + $value .= Utils::chr($code); break; default: @@ -695,7 +717,7 @@ private function readComment($line, $col, Token $prev) do { [$char, $code, $bytes] = $this->moveStringCursor(1, $bytes)->readChar(); $value .= $char; - } while ($code && + } while ($code !== null && // SourceCharacter but not LineTerminator ($code > 0x001F || $code === 0x0009) ); @@ -771,7 +793,7 @@ private function readChars($charCount, $advance = false, $byteStreamPosition = n { $result = ''; $totalBytes = 0; - $byteOffset = $byteStreamPosition ?: $this->byteStreamPosition; + $byteOffset = $byteStreamPosition ?? $this->byteStreamPosition; for ($i = 0; $i < $charCount; $i++) { [$char, $code, $bytes] = $this->readChar(false, $byteOffset); diff --git a/src/Language/Parser.php b/src/Language/Parser.php index f5c928051..9c0f460b3 100644 --- a/src/Language/Parser.php +++ b/src/Language/Parser.php @@ -7,6 +7,7 @@ use GraphQL\Error\SyntaxError; use GraphQL\Language\AST\ArgumentNode; use GraphQL\Language\AST\BooleanValueNode; +use GraphQL\Language\AST\DefinitionNode; use GraphQL\Language\AST\DirectiveDefinitionNode; use GraphQL\Language\AST\DirectiveNode; use GraphQL\Language\AST\DocumentNode; @@ -32,6 +33,7 @@ use GraphQL\Language\AST\Location; use GraphQL\Language\AST\NamedTypeNode; use GraphQL\Language\AST\NameNode; +use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeList; use GraphQL\Language\AST\NonNullTypeNode; use GraphQL\Language\AST\NullValueNode; @@ -45,12 +47,15 @@ use GraphQL\Language\AST\ScalarTypeExtensionNode; use GraphQL\Language\AST\SchemaDefinitionNode; use GraphQL\Language\AST\SchemaTypeExtensionNode; +use GraphQL\Language\AST\SelectionNode; use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Language\AST\StringValueNode; use GraphQL\Language\AST\TypeExtensionNode; +use GraphQL\Language\AST\TypeNode; use GraphQL\Language\AST\TypeSystemDefinitionNode; use GraphQL\Language\AST\UnionTypeDefinitionNode; use GraphQL\Language\AST\UnionTypeExtensionNode; +use GraphQL\Language\AST\ValueNode; use GraphQL\Language\AST\VariableDefinitionNode; use GraphQL\Language\AST\VariableNode; use function count; @@ -58,6 +63,75 @@ /** * Parses string containing GraphQL query or [type definition](type-system/type-language.md) to Abstract Syntax Tree. + * + * Those magic functions allow partial parsing: + * + * @method static NameNode name(Source|string $source, bool[] $options = []) + * @method static DocumentNode document(Source|string $source, bool[] $options = []) + * @method static ExecutableDefinitionNode|TypeSystemDefinitionNode definition(Source|string $source, bool[] $options = []) + * @method static ExecutableDefinitionNode executableDefinition(Source|string $source, bool[] $options = []) + * @method static OperationDefinitionNode operationDefinition(Source|string $source, bool[] $options = []) + * @method static string operationType(Source|string $source, bool[] $options = []) + * @method static NodeList variableDefinitions(Source|string $source, bool[] $options = []) + * @method static VariableDefinitionNode variableDefinition(Source|string $source, bool[] $options = []) + * @method static VariableNode variable(Source|string $source, bool[] $options = []) + * @method static SelectionSetNode selectionSet(Source|string $source, bool[] $options = []) + * @method static mixed selection(Source|string $source, bool[] $options = []) + * @method static FieldNode field(Source|string $source, bool[] $options = []) + * @method static NodeList arguments(Source|string $source, bool[] $options = []) + * @method static NodeList constArguments(Source|string $source, bool[] $options = []) + * @method static ArgumentNode argument(Source|string $source, bool[] $options = []) + * @method static ArgumentNode constArgument(Source|string $source, bool[] $options = []) + * @method static FragmentSpreadNode|InlineFragmentNode fragment(Source|string $source, bool[] $options = []) + * @method static FragmentDefinitionNode fragmentDefinition(Source|string $source, bool[] $options = []) + * @method static NameNode fragmentName(Source|string $source, bool[] $options = []) + * @method static BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|NullValueNode|ObjectValueNode|StringValueNode|VariableNode valueLiteral(Source|string $source, bool[] $options = []) + * @method static BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|NullValueNode|ObjectValueNode|StringValueNode constValueLiteral(Source|string $source, bool[] $options = []) + * @method static StringValueNode stringLiteral(Source|string $source, bool[] $options = []) + * @method static BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|StringValueNode constValue(Source|string $source, bool[] $options = []) + * @method static BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|ObjectValueNode|StringValueNode|VariableNode variableValue(Source|string $source, bool[] $options = []) + * @method static ListValueNode array(Source|string $source, bool[] $options = []) + * @method static ListValueNode constArray(Source|string $source, bool[] $options = []) + * @method static ObjectValueNode object(Source|string $source, bool[] $options = []) + * @method static ObjectValueNode constObject(Source|string $source, bool[] $options = []) + * @method static ObjectFieldNode objectField(Source|string $source, bool[] $options = []) + * @method static ObjectFieldNode constObjectField(Source|string $source, bool[] $options = []) + * @method static NodeList directives(Source|string $source, bool[] $options = []) + * @method static NodeList constDirectives(Source|string $source, bool[] $options = []) + * @method static DirectiveNode directive(Source|string $source, bool[] $options = []) + * @method static DirectiveNode constDirective(Source|string $source, bool[] $options = []) + * @method static ListTypeNode|NamedTypeNode|NonNullTypeNode typeReference(Source|string $source, bool[] $options = []) + * @method static NamedTypeNode namedType(Source|string $source, bool[] $options = []) + * @method static TypeSystemDefinitionNode typeSystemDefinition(Source|string $source, bool[] $options = []) + * @method static StringValueNode|null description(Source|string $source, bool[] $options = []) + * @method static SchemaDefinitionNode schemaDefinition(Source|string $source, bool[] $options = []) + * @method static OperationTypeDefinitionNode operationTypeDefinition(Source|string $source, bool[] $options = []) + * @method static ScalarTypeDefinitionNode scalarTypeDefinition(Source|string $source, bool[] $options = []) + * @method static ObjectTypeDefinitionNode objectTypeDefinition(Source|string $source, bool[] $options = []) + * @method static NodeList implementsInterfaces(Source|string $source, bool[] $options = []) + * @method static NodeList fieldsDefinition(Source|string $source, bool[] $options = []) + * @method static FieldDefinitionNode fieldDefinition(Source|string $source, bool[] $options = []) + * @method static NodeList argumentsDefinition(Source|string $source, bool[] $options = []) + * @method static InputValueDefinitionNode inputValueDefinition(Source|string $source, bool[] $options = []) + * @method static InterfaceTypeDefinitionNode interfaceTypeDefinition(Source|string $source, bool[] $options = []) + * @method static UnionTypeDefinitionNode unionTypeDefinition(Source|string $source, bool[] $options = []) + * @method static NodeList unionMemberTypes(Source|string $source, bool[] $options = []) + * @method static EnumTypeDefinitionNode enumTypeDefinition(Source|string $source, bool[] $options = []) + * @method static NodeList enumValuesDefinition(Source|string $source, bool[] $options = []) + * @method static EnumValueDefinitionNode enumValueDefinition(Source|string $source, bool[] $options = []) + * @method static InputObjectTypeDefinitionNode inputObjectTypeDefinition(Source|string $source, bool[] $options = []) + * @method static NodeList inputFieldsDefinition(Source|string $source, bool[] $options = []) + * @method static TypeExtensionNode typeExtension(Source|string $source, bool[] $options = []) + * @method static SchemaTypeExtensionNode schemaTypeExtension(Source|string $source, bool[] $options = []) + * @method static ScalarTypeExtensionNode scalarTypeExtension(Source|string $source, bool[] $options = []) + * @method static ObjectTypeExtensionNode objectTypeExtension(Source|string $source, bool[] $options = []) + * @method static InterfaceTypeExtensionNode interfaceTypeExtension(Source|string $source, bool[] $options = []) + * @method static UnionTypeExtensionNode unionTypeExtension(Source|string $source, bool[] $options = []) + * @method static EnumTypeExtensionNode enumTypeExtension(Source|string $source, bool[] $options = []) + * @method static InputObjectTypeExtensionNode inputObjectTypeExtension(Source|string $source, bool[] $options = []) + * @method static DirectiveDefinitionNode directiveDefinition(Source|string $source, bool[] $options = []) + * @method static NodeList directiveLocations(Source|string $source, bool[] $options = []) + * @method static NameNode directiveLocation(Source|string $source, bool[] $options = []) */ class Parser { @@ -113,8 +187,7 @@ class Parser */ public static function parse($source, array $options = []) { - $sourceObj = $source instanceof Source ? $source : new Source($source); - $parser = new self($sourceObj, $options); + $parser = new self($source, $options); return $parser->parseDocument(); } @@ -138,8 +211,7 @@ public static function parse($source, array $options = []) */ public static function parseValue($source, array $options = []) { - $sourceObj = $source instanceof Source ? $source : new Source($source); - $parser = new Parser($sourceObj, $options); + $parser = new Parser($source, $options); $parser->expect(Token::SOF); $value = $parser->parseValueLiteral(false); $parser->expect(Token::EOF); @@ -160,14 +232,13 @@ public static function parseValue($source, array $options = []) * @param Source|string $source * @param bool[] $options * - * @return ListTypeNode|NameNode|NonNullTypeNode + * @return ListTypeNode|NamedTypeNode|NonNullTypeNode * * @api */ public static function parseType($source, array $options = []) { - $sourceObj = $source instanceof Source ? $source : new Source($source); - $parser = new Parser($sourceObj, $options); + $parser = new Parser($source, $options); $parser->expect(Token::SOF); $type = $parser->parseTypeReference(); $parser->expect(Token::EOF); @@ -175,26 +246,78 @@ public static function parseType($source, array $options = []) return $type; } + /** + * Parse partial source by delegating calls to the internal parseX methods. + * + * @param bool[] $arguments + * + * @throws SyntaxError + */ + public static function __callStatic(string $name, array $arguments) + { + $parser = new Parser(...$arguments); + $parser->expect(Token::SOF); + + switch ($name) { + case 'arguments': + case 'valueLiteral': + case 'array': + case 'object': + case 'objectField': + case 'directives': + case 'directive': + $type = $parser->{'parse' . $name}(false); + break; + case 'constArguments': + $type = $parser->parseArguments(true); + break; + case 'constValueLiteral': + $type = $parser->parseValueLiteral(true); + break; + case 'constArray': + $type = $parser->parseArray(true); + break; + case 'constObject': + $type = $parser->parseObject(true); + break; + case 'constObjectField': + $type = $parser->parseObjectField(true); + break; + case 'constDirectives': + $type = $parser->parseDirectives(true); + break; + case 'constDirective': + $type = $parser->parseDirective(true); + break; + default: + $type = $parser->{'parse' . $name}(); + } + + $parser->expect(Token::EOF); + + return $type; + } + /** @var Lexer */ private $lexer; /** - * @param bool[] $options + * @param Source|string $source + * @param bool[] $options */ - public function __construct(Source $source, array $options = []) + public function __construct($source, array $options = []) { - $this->lexer = new Lexer($source, $options); + $sourceObj = $source instanceof Source ? $source : new Source($source); + $this->lexer = new Lexer($sourceObj, $options); } /** * Returns a location object, used to identify the place in * the source that created a given parsed object. - * - * @return Location|null */ - private function loc(Token $startToken) + private function loc(Token $startToken) : ?Location { - if (empty($this->lexer->options['noLocation'])) { + if (! ($this->lexer->options['noLocation'] ?? false)) { return new Location($startToken, $this->lexer->lastToken, $this->lexer->source); } @@ -203,12 +326,8 @@ private function loc(Token $startToken) /** * Determines if the next token is of a given kind - * - * @param string $kind - * - * @return bool */ - private function peek($kind) + private function peek(string $kind) : bool { return $this->lexer->token->kind === $kind; } @@ -216,12 +335,8 @@ private function peek($kind) /** * If the next token is of the given kind, return true after advancing * the parser. Otherwise, do not change the parser state and return false. - * - * @param string $kind - * - * @return bool */ - private function skip($kind) + private function skip(string $kind) : bool { $match = $this->lexer->token->kind === $kind; @@ -236,13 +351,9 @@ private function skip($kind) * If the next token is of the given kind, return that token after advancing * the parser. Otherwise, do not change the parser state and return false. * - * @param string $kind - * - * @return Token - * * @throws SyntaxError */ - private function expect($kind) + private function expect(string $kind) : Token { $token = $this->lexer->token; @@ -260,38 +371,44 @@ private function expect($kind) } /** - * If the next token is a keyword with the given value, return that token after - * advancing the parser. Otherwise, do not change the parser state and return - * false. - * - * @param string $value - * - * @return Token + * If the next token is a keyword with the given value, advance the lexer. + * Otherwise, throw an error. * * @throws SyntaxError */ - private function expectKeyword($value) + private function expectKeyword(string $value) : void { $token = $this->lexer->token; + if ($token->kind !== Token::NAME || $token->value !== $value) { + throw new SyntaxError( + $this->lexer->source, + $token->start, + 'Expected "' . $value . '", found ' . $token->getDescription() + ); + } + + $this->lexer->advance(); + } + /** + * If the next token is a given keyword, return "true" after advancing + * the lexer. Otherwise, do not change the parser state and return "false". + */ + private function expectOptionalKeyword(string $value) : bool + { + $token = $this->lexer->token; if ($token->kind === Token::NAME && $token->value === $value) { $this->lexer->advance(); - return $token; + return true; } - throw new SyntaxError( - $this->lexer->source, - $token->start, - 'Expected "' . $value . '", found ' . $token->getDescription() - ); + + return false; } - /** - * @return SyntaxError - */ - private function unexpected(?Token $atToken = null) + private function unexpected(?Token $atToken = null) : SyntaxError { - $token = $atToken ?: $this->lexer->token; + $token = $atToken ?? $this->lexer->token; return new SyntaxError($this->lexer->source, $token->start, 'Unexpected ' . $token->getDescription()); } @@ -302,15 +419,9 @@ private function unexpected(?Token $atToken = null) * and ends with a lex token of closeKind. Advances the parser * to the next lex token after the closing token. * - * @param string $openKind - * @param callable $parseFn - * @param string $closeKind - * - * @return NodeList - * * @throws SyntaxError */ - private function any($openKind, $parseFn, $closeKind) + private function any(string $openKind, callable $parseFn, string $closeKind) : NodeList { $this->expect($openKind); @@ -328,15 +439,9 @@ private function any($openKind, $parseFn, $closeKind) * and ends with a lex token of closeKind. Advances the parser * to the next lex token after the closing token. * - * @param string $openKind - * @param callable $parseFn - * @param string $closeKind - * - * @return NodeList - * * @throws SyntaxError */ - private function many($openKind, $parseFn, $closeKind) + private function many(string $openKind, callable $parseFn, string $closeKind) : NodeList { $this->expect($openKind); @@ -351,11 +456,9 @@ private function many($openKind, $parseFn, $closeKind) /** * Converts a name lex token into a name parse node. * - * @return NameNode - * * @throws SyntaxError */ - private function parseName() + private function parseName() : NameNode { $token = $this->expect(Token::NAME); @@ -368,22 +471,20 @@ private function parseName() /** * Implements the parsing rules in the Document section. * - * @return DocumentNode - * * @throws SyntaxError */ - private function parseDocument() + private function parseDocument() : DocumentNode { $start = $this->lexer->token; - $this->expect(Token::SOF); - - $definitions = []; - do { - $definitions[] = $this->parseDefinition(); - } while (! $this->skip(Token::EOF)); return new DocumentNode([ - 'definitions' => new NodeList($definitions), + 'definitions' => $this->many( + Token::SOF, + function () { + return $this->parseDefinition(); + }, + Token::EOF + ), 'loc' => $this->loc($start), ]); } @@ -393,7 +494,7 @@ private function parseDocument() * * @throws SyntaxError */ - private function parseDefinition() + private function parseDefinition() : DefinitionNode { if ($this->peek(Token::NAME)) { switch ($this->lexer->token->value) { @@ -427,11 +528,9 @@ private function parseDefinition() } /** - * @return ExecutableDefinitionNode - * * @throws SyntaxError */ - private function parseExecutableDefinition() + private function parseExecutableDefinition() : ExecutableDefinitionNode { if ($this->peek(Token::NAME)) { switch ($this->lexer->token->value) { @@ -452,11 +551,9 @@ private function parseExecutableDefinition() // Implements the parsing rules in the Operations section. /** - * @return OperationDefinitionNode - * * @throws SyntaxError */ - private function parseOperationDefinition() + private function parseOperationDefinition() : OperationDefinitionNode { $start = $this->lexer->token; if ($this->peek(Token::BRACE_L)) { @@ -488,11 +585,9 @@ private function parseOperationDefinition() } /** - * @return string - * * @throws SyntaxError */ - private function parseOperationType() + private function parseOperationType() : string { $operationToken = $this->expect(Token::NAME); switch ($operationToken->value) { @@ -507,28 +602,23 @@ private function parseOperationType() throw $this->unexpected($operationToken); } - /** - * @return VariableDefinitionNode[]|NodeList - */ - private function parseVariableDefinitions() + private function parseVariableDefinitions() : NodeList { - return $this->peek(Token::PAREN_L) ? - $this->many( + return $this->peek(Token::PAREN_L) + ? $this->many( Token::PAREN_L, - function () { + function () : VariableDefinitionNode { return $this->parseVariableDefinition(); }, Token::PAREN_R - ) : - new NodeList([]); + ) + : new NodeList([]); } /** - * @return VariableDefinitionNode - * * @throws SyntaxError */ - private function parseVariableDefinition() + private function parseVariableDefinition() : VariableDefinitionNode { $start = $this->lexer->token; $var = $this->parseVariable(); @@ -539,18 +629,18 @@ private function parseVariableDefinition() return new VariableDefinitionNode([ 'variable' => $var, 'type' => $type, - 'defaultValue' => - ($this->skip(Token::EQUALS) ? $this->parseValueLiteral(true) : null), + 'defaultValue' => $this->skip(Token::EQUALS) + ? $this->parseValueLiteral(true) + : null, + 'directives' => $this->parseDirectives(true), 'loc' => $this->loc($start), ]); } /** - * @return VariableNode - * * @throws SyntaxError */ - private function parseVariable() + private function parseVariable() : VariableNode { $start = $this->lexer->token; $this->expect(Token::DOLLAR); @@ -561,10 +651,7 @@ private function parseVariable() ]); } - /** - * @return SelectionSetNode - */ - private function parseSelectionSet() + private function parseSelectionSet() : SelectionSetNode { $start = $this->lexer->token; @@ -572,7 +659,7 @@ private function parseSelectionSet() [ 'selections' => $this->many( Token::BRACE_L, - function () { + function () : SelectionNode { return $this->parseSelection(); }, Token::BRACE_R @@ -587,22 +674,18 @@ function () { * - Field * - FragmentSpread * - InlineFragment - * - * @return mixed */ - private function parseSelection() + private function parseSelection() : SelectionNode { - return $this->peek(Token::SPREAD) ? - $this->parseFragment() : - $this->parseField(); + return $this->peek(Token::SPREAD) + ? $this->parseFragment() + : $this->parseField(); } /** - * @return FieldNode - * * @throws SyntaxError */ - private function parseField() + private function parseField() : FieldNode { $start = $this->lexer->token; $nameOrAlias = $this->parseName(); @@ -626,33 +709,27 @@ private function parseField() } /** - * @param bool $isConst - * - * @return ArgumentNode[]|NodeList - * * @throws SyntaxError */ - private function parseArguments($isConst) + private function parseArguments(bool $isConst) : NodeList { - $parseFn = $isConst ? - function () { + $parseFn = $isConst + ? function () : ArgumentNode { return $this->parseConstArgument(); - } : - function () { + } + : function () : ArgumentNode { return $this->parseArgument(); }; - return $this->peek(Token::PAREN_L) ? - $this->many(Token::PAREN_L, $parseFn, Token::PAREN_R) : - new NodeList([]); + return $this->peek(Token::PAREN_L) + ? $this->many(Token::PAREN_L, $parseFn, Token::PAREN_R) + : new NodeList([]); } /** - * @return ArgumentNode - * * @throws SyntaxError */ - private function parseArgument() + private function parseArgument() : ArgumentNode { $start = $this->lexer->token; $name = $this->parseName(); @@ -668,11 +745,9 @@ private function parseArgument() } /** - * @return ArgumentNode - * * @throws SyntaxError */ - private function parseConstArgument() + private function parseConstArgument() : ArgumentNode { $start = $this->lexer->token; $name = $this->parseName(); @@ -694,12 +769,13 @@ private function parseConstArgument() * * @throws SyntaxError */ - private function parseFragment() + private function parseFragment() : SelectionNode { $start = $this->lexer->token; $this->expect(Token::SPREAD); - if ($this->peek(Token::NAME) && $this->lexer->token->value !== 'on') { + $hasTypeCondition = $this->expectOptionalKeyword('on'); + if (! $hasTypeCondition && $this->peek(Token::NAME)) { return new FragmentSpreadNode([ 'name' => $this->parseFragmentName(), 'directives' => $this->parseDirectives(false), @@ -707,14 +783,8 @@ private function parseFragment() ]); } - $typeCondition = null; - if ($this->lexer->token->value === 'on') { - $this->lexer->advance(); - $typeCondition = $this->parseNamedType(); - } - return new InlineFragmentNode([ - 'typeCondition' => $typeCondition, + 'typeCondition' => $hasTypeCondition ? $this->parseNamedType() : null, 'directives' => $this->parseDirectives(false), 'selectionSet' => $this->parseSelectionSet(), 'loc' => $this->loc($start), @@ -722,11 +792,9 @@ private function parseFragment() } /** - * @return FragmentDefinitionNode - * * @throws SyntaxError */ - private function parseFragmentDefinition() + private function parseFragmentDefinition() : FragmentDefinitionNode { $start = $this->lexer->token; $this->expectKeyword('fragment'); @@ -754,11 +822,9 @@ private function parseFragmentDefinition() } /** - * @return NameNode - * * @throws SyntaxError */ - private function parseFragmentName() + private function parseFragmentName() : NameNode { if ($this->lexer->token->value === 'on') { throw $this->unexpected(); @@ -787,13 +853,11 @@ private function parseFragmentName() * * EnumValue : Name but not `true`, `false` or `null` * - * @param bool $isConst - * * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|StringValueNode|VariableNode|ListValueNode|ObjectValueNode|NullValueNode * * @throws SyntaxError */ - private function parseValueLiteral($isConst) + private function parseValueLiteral(bool $isConst) : ValueNode { $token = $this->lexer->token; switch ($token->kind) { @@ -853,10 +917,7 @@ private function parseValueLiteral($isConst) throw $this->unexpected(); } - /** - * @return StringValueNode - */ - private function parseStringLiteral() + private function parseStringLiteral() : StringValueNode { $token = $this->lexer->token; $this->lexer->advance(); @@ -873,7 +934,7 @@ private function parseStringLiteral() * * @throws SyntaxError */ - private function parseConstValue() + private function parseConstValue() : ValueNode { return $this->parseValueLiteral(true); } @@ -881,24 +942,21 @@ private function parseConstValue() /** * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|ObjectValueNode|StringValueNode|VariableNode */ - private function parseVariableValue() + private function parseVariableValue() : ValueNode { return $this->parseValueLiteral(false); } - /** - * @param bool $isConst - * - * @return ListValueNode - */ - private function parseArray($isConst) + private function parseArray(bool $isConst) : ListValueNode { $start = $this->lexer->token; - $parseFn = $isConst ? function () { - return $this->parseConstValue(); - } : function () { - return $this->parseVariableValue(); - }; + $parseFn = $isConst + ? function () { + return $this->parseConstValue(); + } + : function () { + return $this->parseVariableValue(); + }; return new ListValueNode( [ @@ -908,12 +966,7 @@ private function parseArray($isConst) ); } - /** - * @param bool $isConst - * - * @return ObjectValueNode - */ - private function parseObject($isConst) + private function parseObject(bool $isConst) : ObjectValueNode { $start = $this->lexer->token; $this->expect(Token::BRACE_L); @@ -928,12 +981,7 @@ private function parseObject($isConst) ]); } - /** - * @param bool $isConst - * - * @return ObjectFieldNode - */ - private function parseObjectField($isConst) + private function parseObjectField(bool $isConst) : ObjectFieldNode { $start = $this->lexer->token; $name = $this->parseName(); @@ -950,13 +998,9 @@ private function parseObjectField($isConst) // Implements the parsing rules in the Directives section. /** - * @param bool $isConst - * - * @return DirectiveNode[]|NodeList - * * @throws SyntaxError */ - private function parseDirectives($isConst) + private function parseDirectives(bool $isConst) : NodeList { $directives = []; while ($this->peek(Token::AT)) { @@ -967,13 +1011,9 @@ private function parseDirectives($isConst) } /** - * @param bool $isConst - * - * @return DirectiveNode - * * @throws SyntaxError */ - private function parseDirective($isConst) + private function parseDirective(bool $isConst) : DirectiveNode { $start = $this->lexer->token; $this->expect(Token::AT); @@ -990,11 +1030,11 @@ private function parseDirective($isConst) /** * Handles the Type: TypeName, ListType, and NonNullType parsing rules. * - * @return ListTypeNode|NameNode|NonNullTypeNode + * @return ListTypeNode|NamedTypeNode|NonNullTypeNode * * @throws SyntaxError */ - private function parseTypeReference() + private function parseTypeReference() : TypeNode { $start = $this->lexer->token; @@ -1018,7 +1058,7 @@ private function parseTypeReference() return $type; } - private function parseNamedType() + private function parseNamedType() : NamedTypeNode { $start = $this->lexer->token; @@ -1045,11 +1085,9 @@ private function parseNamedType() * - EnumTypeDefinition * - InputObjectTypeDefinition * - * @return TypeSystemDefinitionNode - * * @throws SyntaxError */ - private function parseTypeSystemDefinition() + private function parseTypeSystemDefinition() : TypeSystemDefinitionNode { // Many definitions begin with a description and require a lookahead. $keywordToken = $this->peekDescription() @@ -1082,30 +1120,24 @@ private function parseTypeSystemDefinition() throw $this->unexpected($keywordToken); } - /** - * @return bool - */ - private function peekDescription() + private function peekDescription() : bool { return $this->peek(Token::STRING) || $this->peek(Token::BLOCK_STRING); } - /** - * @return StringValueNode|null - */ - private function parseDescription() + private function parseDescription() : ?StringValueNode { if ($this->peekDescription()) { return $this->parseStringLiteral(); } + + return null; } /** - * @return SchemaDefinitionNode - * * @throws SyntaxError */ - private function parseSchemaDefinition() + private function parseSchemaDefinition() : SchemaDefinitionNode { $start = $this->lexer->token; $this->expectKeyword('schema'); @@ -1113,7 +1145,7 @@ private function parseSchemaDefinition() $operationTypes = $this->many( Token::BRACE_L, - function () { + function () : OperationTypeDefinitionNode { return $this->parseOperationTypeDefinition(); }, Token::BRACE_R @@ -1127,11 +1159,9 @@ function () { } /** - * @return OperationTypeDefinitionNode - * * @throws SyntaxError */ - private function parseOperationTypeDefinition() + private function parseOperationTypeDefinition() : OperationTypeDefinitionNode { $start = $this->lexer->token; $operation = $this->parseOperationType(); @@ -1146,11 +1176,9 @@ private function parseOperationTypeDefinition() } /** - * @return ScalarTypeDefinitionNode - * * @throws SyntaxError */ - private function parseScalarTypeDefinition() + private function parseScalarTypeDefinition() : ScalarTypeDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); @@ -1167,11 +1195,9 @@ private function parseScalarTypeDefinition() } /** - * @return ObjectTypeDefinitionNode - * * @throws SyntaxError */ - private function parseObjectTypeDefinition() + private function parseObjectTypeDefinition() : ObjectTypeDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); @@ -1195,67 +1221,64 @@ private function parseObjectTypeDefinition() * ImplementsInterfaces : * - implements `&`? NamedType * - ImplementsInterfaces & NamedType - * - * @return NamedTypeNode[] */ - private function parseImplementsInterfaces() + private function parseImplementsInterfaces() : NodeList { $types = []; - if ($this->lexer->token->value === 'implements') { - $this->lexer->advance(); + if ($this->expectOptionalKeyword('implements')) { // Optional leading ampersand $this->skip(Token::AMP); do { $types[] = $this->parseNamedType(); } while ($this->skip(Token::AMP) || - // Legacy support for the SDL? - (! empty($this->lexer->options['allowLegacySDLImplementsInterfaces']) && $this->peek(Token::NAME)) + // Legacy support for the SDL? + (($this->lexer->options['allowLegacySDLImplementsInterfaces'] ?? false) && $this->peek(Token::NAME)) ); } - return $types; + return new NodeList($types); } /** - * @return FieldDefinitionNode[]|NodeList - * * @throws SyntaxError */ - private function parseFieldsDefinition() + private function parseFieldsDefinition() : NodeList { // Legacy support for the SDL? - if (! empty($this->lexer->options['allowLegacySDLEmptyFields']) && - $this->peek(Token::BRACE_L) && - $this->lexer->lookahead()->kind === Token::BRACE_R + if (($this->lexer->options['allowLegacySDLEmptyFields'] ?? false) + && $this->peek(Token::BRACE_L) + && $this->lexer->lookahead()->kind === Token::BRACE_R ) { $this->lexer->advance(); $this->lexer->advance(); - return []; + /** @phpstan-var NodeList $nodeList */ + $nodeList = new NodeList([]); + } else { + /** @phpstan-var NodeList $nodeList */ + $nodeList = $this->peek(Token::BRACE_L) + ? $this->many( + Token::BRACE_L, + function () : FieldDefinitionNode { + return $this->parseFieldDefinition(); + }, + Token::BRACE_R + ) + : new NodeList([]); } - return $this->peek(Token::BRACE_L) - ? $this->many( - Token::BRACE_L, - function () { - return $this->parseFieldDefinition(); - }, - Token::BRACE_R - ) - : new NodeList([]); + return $nodeList; } /** - * @return FieldDefinitionNode - * * @throws SyntaxError */ - private function parseFieldDefinition() + private function parseFieldDefinition() : FieldDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); $name = $this->parseName(); - $args = $this->parseArgumentDefs(); + $args = $this->parseArgumentsDefinition(); $this->expect(Token::COLON); $type = $this->parseTypeReference(); $directives = $this->parseDirectives(true); @@ -1271,31 +1294,28 @@ private function parseFieldDefinition() } /** - * @return InputValueDefinitionNode[]|NodeList - * * @throws SyntaxError */ - private function parseArgumentDefs() + private function parseArgumentsDefinition() : NodeList { - if (! $this->peek(Token::PAREN_L)) { - return new NodeList([]); - } + /** @var NodeList $nodeList */ + $nodeList = $this->peek(Token::PAREN_L) + ? $this->many( + Token::PAREN_L, + function () : InputValueDefinitionNode { + return $this->parseInputValueDefinition(); + }, + Token::PAREN_R + ) + : new NodeList([]); - return $this->many( - Token::PAREN_L, - function () { - return $this->parseInputValueDef(); - }, - Token::PAREN_R - ); + return $nodeList; } /** - * @return InputValueDefinitionNode - * * @throws SyntaxError */ - private function parseInputValueDef() + private function parseInputValueDefinition() : InputValueDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); @@ -1319,22 +1339,22 @@ private function parseInputValueDef() } /** - * @return InterfaceTypeDefinitionNode - * * @throws SyntaxError */ - private function parseInterfaceTypeDefinition() + private function parseInterfaceTypeDefinition() : InterfaceTypeDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); $this->expectKeyword('interface'); $name = $this->parseName(); + $interfaces = $this->parseImplementsInterfaces(); $directives = $this->parseDirectives(true); $fields = $this->parseFieldsDefinition(); return new InterfaceTypeDefinitionNode([ 'name' => $name, 'directives' => $directives, + 'interfaces' => $interfaces, 'fields' => $fields, 'loc' => $this->loc($start), 'description' => $description, @@ -1345,11 +1365,9 @@ private function parseInterfaceTypeDefinition() * UnionTypeDefinition : * - Description? union Name Directives[Const]? UnionMemberTypes? * - * @return UnionTypeDefinitionNode - * * @throws SyntaxError */ - private function parseUnionTypeDefinition() + private function parseUnionTypeDefinition() : UnionTypeDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); @@ -1371,10 +1389,8 @@ private function parseUnionTypeDefinition() * UnionMemberTypes : * - = `|`? NamedType * - UnionMemberTypes | NamedType - * - * @return NamedTypeNode[] */ - private function parseUnionMemberTypes() + private function parseUnionMemberTypes() : NodeList { $types = []; if ($this->skip(Token::EQUALS)) { @@ -1385,15 +1401,13 @@ private function parseUnionMemberTypes() } while ($this->skip(Token::PIPE)); } - return $types; + return new NodeList($types); } /** - * @return EnumTypeDefinitionNode - * * @throws SyntaxError */ - private function parseEnumTypeDefinition() + private function parseEnumTypeDefinition() : EnumTypeDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); @@ -1412,29 +1426,28 @@ private function parseEnumTypeDefinition() } /** - * @return EnumValueDefinitionNode[]|NodeList - * * @throws SyntaxError */ - private function parseEnumValuesDefinition() + private function parseEnumValuesDefinition() : NodeList { - return $this->peek(Token::BRACE_L) + /** @var NodeList $nodeList */ + $nodeList = $this->peek(Token::BRACE_L) ? $this->many( Token::BRACE_L, - function () { + function () : EnumValueDefinitionNode { return $this->parseEnumValueDefinition(); }, Token::BRACE_R ) : new NodeList([]); + + return $nodeList; } /** - * @return EnumValueDefinitionNode - * * @throws SyntaxError */ - private function parseEnumValueDefinition() + private function parseEnumValueDefinition() : EnumValueDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); @@ -1450,11 +1463,9 @@ private function parseEnumValueDefinition() } /** - * @return InputObjectTypeDefinitionNode - * * @throws SyntaxError */ - private function parseInputObjectTypeDefinition() + private function parseInputObjectTypeDefinition() : InputObjectTypeDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); @@ -1473,21 +1484,22 @@ private function parseInputObjectTypeDefinition() } /** - * @return InputValueDefinitionNode[]|NodeList - * * @throws SyntaxError */ - private function parseInputFieldsDefinition() + private function parseInputFieldsDefinition() : NodeList { - return $this->peek(Token::BRACE_L) + /** @var NodeList $nodeList */ + $nodeList = $this->peek(Token::BRACE_L) ? $this->many( Token::BRACE_L, - function () { - return $this->parseInputValueDef(); + function () : InputValueDefinitionNode { + return $this->parseInputValueDefinition(); }, Token::BRACE_R ) : new NodeList([]); + + return $nodeList; } /** @@ -1499,11 +1511,9 @@ function () { * - EnumTypeExtension * - InputObjectTypeDefinition * - * @return TypeExtensionNode - * * @throws SyntaxError */ - private function parseTypeExtension() + private function parseTypeExtension() : TypeExtensionNode { $keywordToken = $this->lexer->lookahead(); @@ -1530,11 +1540,9 @@ private function parseTypeExtension() } /** - * @return SchemaTypeExtensionNode - * * @throws SyntaxError */ - private function parseSchemaTypeExtension() + private function parseSchemaTypeExtension() : SchemaTypeExtensionNode { $start = $this->lexer->token; $this->expectKeyword('extend'); @@ -1545,7 +1553,8 @@ private function parseSchemaTypeExtension() Token::BRACE_L, [$this, 'parseOperationTypeDefinition'], Token::BRACE_R - ) : []; + ) + : new NodeList([]); if (count($directives) === 0 && count($operationTypes) === 0) { $this->unexpected(); } @@ -1558,11 +1567,9 @@ private function parseSchemaTypeExtension() } /** - * @return ScalarTypeExtensionNode - * * @throws SyntaxError */ - private function parseScalarTypeExtension() + private function parseScalarTypeExtension() : ScalarTypeExtensionNode { $start = $this->lexer->token; $this->expectKeyword('extend'); @@ -1581,11 +1588,9 @@ private function parseScalarTypeExtension() } /** - * @return ObjectTypeExtensionNode - * * @throws SyntaxError */ - private function parseObjectTypeExtension() + private function parseObjectTypeExtension() : ObjectTypeExtensionNode { $start = $this->lexer->token; $this->expectKeyword('extend'); @@ -1612,20 +1617,20 @@ private function parseObjectTypeExtension() } /** - * @return InterfaceTypeExtensionNode - * * @throws SyntaxError */ - private function parseInterfaceTypeExtension() + private function parseInterfaceTypeExtension() : InterfaceTypeExtensionNode { $start = $this->lexer->token; $this->expectKeyword('extend'); $this->expectKeyword('interface'); $name = $this->parseName(); + $interfaces = $this->parseImplementsInterfaces(); $directives = $this->parseDirectives(true); $fields = $this->parseFieldsDefinition(); - if (count($directives) === 0 && - count($fields) === 0 + if (count($interfaces) === 0 + && count($directives) === 0 + && count($fields) === 0 ) { throw $this->unexpected(); } @@ -1633,6 +1638,7 @@ private function parseInterfaceTypeExtension() return new InterfaceTypeExtensionNode([ 'name' => $name, 'directives' => $directives, + 'interfaces' => $interfaces, 'fields' => $fields, 'loc' => $this->loc($start), ]); @@ -1643,11 +1649,9 @@ private function parseInterfaceTypeExtension() * - extend union Name Directives[Const]? UnionMemberTypes * - extend union Name Directives[Const] * - * @return UnionTypeExtensionNode - * * @throws SyntaxError */ - private function parseUnionTypeExtension() + private function parseUnionTypeExtension() : UnionTypeExtensionNode { $start = $this->lexer->token; $this->expectKeyword('extend'); @@ -1655,9 +1659,7 @@ private function parseUnionTypeExtension() $name = $this->parseName(); $directives = $this->parseDirectives(true); $types = $this->parseUnionMemberTypes(); - if (count($directives) === 0 && - ! $types - ) { + if (count($directives) === 0 && count($types) === 0) { throw $this->unexpected(); } @@ -1670,11 +1672,9 @@ private function parseUnionTypeExtension() } /** - * @return EnumTypeExtensionNode - * * @throws SyntaxError */ - private function parseEnumTypeExtension() + private function parseEnumTypeExtension() : EnumTypeExtensionNode { $start = $this->lexer->token; $this->expectKeyword('extend'); @@ -1697,11 +1697,9 @@ private function parseEnumTypeExtension() } /** - * @return InputObjectTypeExtensionNode - * * @throws SyntaxError */ - private function parseInputObjectTypeExtension() + private function parseInputObjectTypeExtension() : InputObjectTypeExtensionNode { $start = $this->lexer->token; $this->expectKeyword('extend'); @@ -1725,38 +1723,36 @@ private function parseInputObjectTypeExtension() /** * DirectiveDefinition : - * - directive @ Name ArgumentsDefinition? on DirectiveLocations - * - * @return DirectiveDefinitionNode + * - Description? directive @ Name ArgumentsDefinition? `repeatable`? on DirectiveLocations * * @throws SyntaxError */ - private function parseDirectiveDefinition() + private function parseDirectiveDefinition() : DirectiveDefinitionNode { $start = $this->lexer->token; $description = $this->parseDescription(); $this->expectKeyword('directive'); $this->expect(Token::AT); - $name = $this->parseName(); - $args = $this->parseArgumentDefs(); + $name = $this->parseName(); + $args = $this->parseArgumentsDefinition(); + $repeatable = $this->expectOptionalKeyword('repeatable'); $this->expectKeyword('on'); $locations = $this->parseDirectiveLocations(); return new DirectiveDefinitionNode([ 'name' => $name, + 'description' => $description, 'arguments' => $args, + 'repeatable' => $repeatable, 'locations' => $locations, 'loc' => $this->loc($start), - 'description' => $description, ]); } /** - * @return NameNode[] - * * @throws SyntaxError */ - private function parseDirectiveLocations() + private function parseDirectiveLocations() : NodeList { // Optional leading pipe $this->skip(Token::PIPE); @@ -1765,15 +1761,13 @@ private function parseDirectiveLocations() $locations[] = $this->parseDirectiveLocation(); } while ($this->skip(Token::PIPE)); - return $locations; + return new NodeList($locations); } /** - * @return NameNode - * * @throws SyntaxError */ - private function parseDirectiveLocation() + private function parseDirectiveLocation() : NameNode { $start = $this->lexer->token; $name = $this->parseName(); diff --git a/src/Language/Printer.php b/src/Language/Printer.php index ae96222fc..0a95efc44 100644 --- a/src/Language/Printer.php +++ b/src/Language/Printer.php @@ -28,6 +28,7 @@ use GraphQL\Language\AST\ListTypeNode; use GraphQL\Language\AST\ListValueNode; use GraphQL\Language\AST\NamedTypeNode; +use GraphQL\Language\AST\NameNode; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NonNullTypeNode; @@ -47,6 +48,7 @@ use GraphQL\Language\AST\UnionTypeDefinitionNode; use GraphQL\Language\AST\UnionTypeExtensionNode; use GraphQL\Language\AST\VariableDefinitionNode; +use GraphQL\Language\AST\VariableNode; use GraphQL\Utils\Utils; use function count; use function implode; @@ -54,6 +56,7 @@ use function preg_replace; use function sprintf; use function str_replace; +use function strlen; use function strpos; /** @@ -82,7 +85,7 @@ class Printer public static function doPrint($ast) { static $instance; - $instance = $instance ?: new static(); + $instance = $instance ?? new static(); return $instance->printAST($ast); } @@ -91,25 +94,31 @@ protected function __construct() { } + /** + * Traverse an AST bottom-up, converting all nodes to strings. + * + * That means the AST is manipulated in such a way that it no longer + * resembles the well-formed result of parsing. + */ public function printAST($ast) { return Visitor::visit( $ast, [ 'leave' => [ - NodeKind::NAME => static function (Node $node) { - return '' . $node->value; + NodeKind::NAME => static function (NameNode $node) : string { + return $node->value; }, - NodeKind::VARIABLE => static function ($node) { + NodeKind::VARIABLE => static function (VariableNode $node) : string { return '$' . $node->name; }, - NodeKind::DOCUMENT => function (DocumentNode $node) { + NodeKind::DOCUMENT => function (DocumentNode $node) : string { return $this->join($node->definitions, "\n\n") . "\n"; }, - NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) { + NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) : string { $op = $node->operation; $name = $node->name; $varDefs = $this->wrap('(', $this->join($node->variableDefinitions, ', '), ')'); @@ -118,20 +127,24 @@ public function printAST($ast) // Anonymous queries with no directives or variable definitions can use // the query short form. - return ! $name && ! $directives && ! $varDefs && $op === 'query' + return $name === null && strlen($directives ?? '') === 0 && ! $varDefs && $op === 'query' ? $selectionSet : $this->join([$op, $this->join([$name, $varDefs]), $directives, $selectionSet], ' '); }, - NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) { - return $node->variable . ': ' . $node->type . $this->wrap(' = ', $node->defaultValue); + NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) : string { + return $node->variable + . ': ' + . $node->type + . $this->wrap(' = ', $node->defaultValue) + . $this->wrap(' ', $this->join($node->directives, ' ')); }, NodeKind::SELECTION_SET => function (SelectionSetNode $node) { return $this->block($node->selections); }, - NodeKind::FIELD => function (FieldNode $node) { + NodeKind::FIELD => function (FieldNode $node) : string { return $this->join( [ $this->wrap('', $node->alias, ': ') . $node->name . $this->wrap( @@ -146,15 +159,17 @@ public function printAST($ast) ); }, - NodeKind::ARGUMENT => static function (ArgumentNode $node) { + NodeKind::ARGUMENT => static function (ArgumentNode $node) : string { return $node->name . ': ' . $node->value; }, - NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) { - return '...' . $node->name . $this->wrap(' ', $this->join($node->directives, ' ')); + NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) : string { + return '...' + . $node->name + . $this->wrap(' ', $this->join($node->directives, ' ')); }, - NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) { + NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) : string { return $this->join( [ '...', @@ -166,7 +181,7 @@ public function printAST($ast) ); }, - NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) { + NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) : string { // Note: fragment variable definitions are experimental and may be changed or removed in the future. return sprintf('fragment %s', $node->name) . $this->wrap('(', $this->join($node->variableDefinitions, ', '), ')') @@ -175,15 +190,15 @@ public function printAST($ast) . $node->selectionSet; }, - NodeKind::INT => static function (IntValueNode $node) { + NodeKind::INT => static function (IntValueNode $node) : string { return $node->value; }, - NodeKind::FLOAT => static function (FloatValueNode $node) { + NodeKind::FLOAT => static function (FloatValueNode $node) : string { return $node->value; }, - NodeKind::STRING => function (StringValueNode $node, $key) { + NodeKind::STRING => function (StringValueNode $node, $key) : string { if ($node->block) { return $this->printBlockString($node->value, $key === 'description'); } @@ -191,47 +206,48 @@ public function printAST($ast) return json_encode($node->value); }, - NodeKind::BOOLEAN => static function (BooleanValueNode $node) { + NodeKind::BOOLEAN => static function (BooleanValueNode $node) : string { return $node->value ? 'true' : 'false'; }, - NodeKind::NULL => static function (NullValueNode $node) { + NodeKind::NULL => static function (NullValueNode $node) : string { return 'null'; }, - NodeKind::ENUM => static function (EnumValueNode $node) { + NodeKind::ENUM => static function (EnumValueNode $node) : string { return $node->value; }, - NodeKind::LST => function (ListValueNode $node) { + NodeKind::LST => function (ListValueNode $node) : string { return '[' . $this->join($node->values, ', ') . ']'; }, - NodeKind::OBJECT => function (ObjectValueNode $node) { + NodeKind::OBJECT => function (ObjectValueNode $node) : string { return '{' . $this->join($node->fields, ', ') . '}'; }, - NodeKind::OBJECT_FIELD => static function (ObjectFieldNode $node) { + NodeKind::OBJECT_FIELD => static function (ObjectFieldNode $node) : string { return $node->name . ': ' . $node->value; }, - NodeKind::DIRECTIVE => function (DirectiveNode $node) { + NodeKind::DIRECTIVE => function (DirectiveNode $node) : string { return '@' . $node->name . $this->wrap('(', $this->join($node->arguments, ', '), ')'); }, - NodeKind::NAMED_TYPE => static function (NamedTypeNode $node) { + NodeKind::NAMED_TYPE => static function (NamedTypeNode $node) : string { + // @phpstan-ignore-next-line the printer works bottom up, so this is already a string here return $node->name; }, - NodeKind::LIST_TYPE => static function (ListTypeNode $node) { + NodeKind::LIST_TYPE => static function (ListTypeNode $node) : string { return '[' . $node->type . ']'; }, - NodeKind::NON_NULL_TYPE => static function (NonNullTypeNode $node) { + NodeKind::NON_NULL_TYPE => static function (NonNullTypeNode $node) : string { return $node->type . '!'; }, - NodeKind::SCHEMA_DEFINITION => function (SchemaDefinitionNode $def) { + NodeKind::SCHEMA_DEFINITION => function (SchemaDefinitionNode $def) : string { return $this->join( [ 'schema', @@ -242,15 +258,15 @@ public function printAST($ast) ); }, - NodeKind::OPERATION_TYPE_DEFINITION => static function (OperationTypeDefinitionNode $def) { + NodeKind::OPERATION_TYPE_DEFINITION => static function (OperationTypeDefinitionNode $def) : string { return $def->operation . ': ' . $def->type; }, - NodeKind::SCALAR_TYPE_DEFINITION => $this->addDescription(function (ScalarTypeDefinitionNode $def) { + NodeKind::SCALAR_TYPE_DEFINITION => $this->addDescription(function (ScalarTypeDefinitionNode $def) : string { return $this->join(['scalar', $def->name, $this->join($def->directives, ' ')], ' '); }), - NodeKind::OBJECT_TYPE_DEFINITION => $this->addDescription(function (ObjectTypeDefinitionNode $def) { + NodeKind::OBJECT_TYPE_DEFINITION => $this->addDescription(function (ObjectTypeDefinitionNode $def) : string { return $this->join( [ 'type', @@ -263,8 +279,8 @@ public function printAST($ast) ); }), - NodeKind::FIELD_DEFINITION => $this->addDescription(function (FieldDefinitionNode $def) { - $noIndent = Utils::every($def->arguments, static function (string $arg) { + NodeKind::FIELD_DEFINITION => $this->addDescription(function (FieldDefinitionNode $def) : string { + $noIndent = Utils::every($def->arguments, static function (string $arg) : bool { return strpos($arg, "\n") === false; }); @@ -276,7 +292,7 @@ public function printAST($ast) . $this->wrap(' ', $this->join($def->directives, ' ')); }), - NodeKind::INPUT_VALUE_DEFINITION => $this->addDescription(function (InputValueDefinitionNode $def) { + NodeKind::INPUT_VALUE_DEFINITION => $this->addDescription(function (InputValueDefinitionNode $def) : string { return $this->join( [ $def->name . ': ' . $def->type, @@ -288,11 +304,12 @@ public function printAST($ast) }), NodeKind::INTERFACE_TYPE_DEFINITION => $this->addDescription( - function (InterfaceTypeDefinitionNode $def) { + function (InterfaceTypeDefinitionNode $def) : string { return $this->join( [ 'interface', $def->name, + $this->wrap('implements ', $this->join($def->interfaces, ' & ')), $this->join($def->directives, ' '), $this->block($def->fields), ], @@ -301,13 +318,13 @@ function (InterfaceTypeDefinitionNode $def) { } ), - NodeKind::UNION_TYPE_DEFINITION => $this->addDescription(function (UnionTypeDefinitionNode $def) { + NodeKind::UNION_TYPE_DEFINITION => $this->addDescription(function (UnionTypeDefinitionNode $def) : string { return $this->join( [ 'union', $def->name, $this->join($def->directives, ' '), - $def->types + count($def->types ?? []) > 0 ? '= ' . $this->join($def->types, ' | ') : '', ], @@ -315,7 +332,7 @@ function (InterfaceTypeDefinitionNode $def) { ); }), - NodeKind::ENUM_TYPE_DEFINITION => $this->addDescription(function (EnumTypeDefinitionNode $def) { + NodeKind::ENUM_TYPE_DEFINITION => $this->addDescription(function (EnumTypeDefinitionNode $def) : string { return $this->join( [ 'enum', @@ -327,13 +344,13 @@ function (InterfaceTypeDefinitionNode $def) { ); }), - NodeKind::ENUM_VALUE_DEFINITION => $this->addDescription(function (EnumValueDefinitionNode $def) { + NodeKind::ENUM_VALUE_DEFINITION => $this->addDescription(function (EnumValueDefinitionNode $def) : string { return $this->join([$def->name, $this->join($def->directives, ' ')], ' '); }), NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $this->addDescription(function ( InputObjectTypeDefinitionNode $def - ) { + ) : string { return $this->join( [ 'input', @@ -345,7 +362,7 @@ function (InterfaceTypeDefinitionNode $def) { ); }), - NodeKind::SCHEMA_EXTENSION => function (SchemaTypeExtensionNode $def) { + NodeKind::SCHEMA_EXTENSION => function (SchemaTypeExtensionNode $def) : string { return $this->join( [ 'extend schema', @@ -356,7 +373,7 @@ function (InterfaceTypeDefinitionNode $def) { ); }, - NodeKind::SCALAR_TYPE_EXTENSION => function (ScalarTypeExtensionNode $def) { + NodeKind::SCALAR_TYPE_EXTENSION => function (ScalarTypeExtensionNode $def) : string { return $this->join( [ 'extend scalar', @@ -367,7 +384,7 @@ function (InterfaceTypeDefinitionNode $def) { ); }, - NodeKind::OBJECT_TYPE_EXTENSION => function (ObjectTypeExtensionNode $def) { + NodeKind::OBJECT_TYPE_EXTENSION => function (ObjectTypeExtensionNode $def) : string { return $this->join( [ 'extend type', @@ -380,11 +397,12 @@ function (InterfaceTypeDefinitionNode $def) { ); }, - NodeKind::INTERFACE_TYPE_EXTENSION => function (InterfaceTypeExtensionNode $def) { + NodeKind::INTERFACE_TYPE_EXTENSION => function (InterfaceTypeExtensionNode $def) : string { return $this->join( [ 'extend interface', $def->name, + $this->wrap('implements ', $this->join($def->interfaces, ' & ')), $this->join($def->directives, ' '), $this->block($def->fields), ], @@ -392,13 +410,13 @@ function (InterfaceTypeDefinitionNode $def) { ); }, - NodeKind::UNION_TYPE_EXTENSION => function (UnionTypeExtensionNode $def) { + NodeKind::UNION_TYPE_EXTENSION => function (UnionTypeExtensionNode $def) : string { return $this->join( [ 'extend union', $def->name, $this->join($def->directives, ' '), - $def->types + count($def->types ?? []) > 0 ? '= ' . $this->join($def->types, ' | ') : '', ], @@ -406,7 +424,7 @@ function (InterfaceTypeDefinitionNode $def) { ); }, - NodeKind::ENUM_TYPE_EXTENSION => function (EnumTypeExtensionNode $def) { + NodeKind::ENUM_TYPE_EXTENSION => function (EnumTypeExtensionNode $def) : string { return $this->join( [ 'extend enum', @@ -418,7 +436,7 @@ function (InterfaceTypeDefinitionNode $def) { ); }, - NodeKind::INPUT_OBJECT_TYPE_EXTENSION => function (InputObjectTypeExtensionNode $def) { + NodeKind::INPUT_OBJECT_TYPE_EXTENSION => function (InputObjectTypeExtensionNode $def) : string { return $this->join( [ 'extend input', @@ -430,8 +448,8 @@ function (InterfaceTypeDefinitionNode $def) { ); }, - NodeKind::DIRECTIVE_DEFINITION => $this->addDescription(function (DirectiveDefinitionNode $def) { - $noIndent = Utils::every($def->arguments, static function (string $arg) { + NodeKind::DIRECTIVE_DEFINITION => $this->addDescription(function (DirectiveDefinitionNode $def) : string { + $noIndent = Utils::every($def->arguments, static function (string $arg) : bool { return strpos($arg, "\n") === false; }); @@ -440,6 +458,7 @@ function (InterfaceTypeDefinitionNode $def) { . ($noIndent ? $this->wrap('(', $this->join($def->arguments, ', '), ')') : $this->wrap("(\n", $this->indent($this->join($def->arguments, "\n")), "\n")) + . ($def->repeatable ? ' repeatable' : '') . ' on ' . $this->join($def->locations, ' | '); }), ], @@ -449,7 +468,7 @@ function (InterfaceTypeDefinitionNode $def) { public function addDescription(callable $cb) { - return function ($node) use ($cb) { + return function ($node) use ($cb) : string { return $this->join([$node->description, $cb($node)], "\n"); }; } @@ -489,14 +508,14 @@ public function length($maybeArray) return $maybeArray ? count($maybeArray) : 0; } - public function join($maybeArray, $separator = '') + public function join($maybeArray, $separator = '') : string { return $maybeArray ? implode( $separator, Utils::filter( $maybeArray, - static function ($x) { + static function ($x) : bool { return (bool) $x; } ) diff --git a/src/Language/Source.php b/src/Language/Source.php index 7e940305e..bd8794076 100644 --- a/src/Language/Source.php +++ b/src/Language/Source.php @@ -46,8 +46,8 @@ public function __construct($body, $name = null, ?SourceLocation $location = nul $this->body = $body; $this->length = mb_strlen($body, 'UTF-8'); - $this->name = $name ?: 'GraphQL request'; - $this->locationOffset = $location ?: new SourceLocation(1, 1); + $this->name = $name === '' || $name === null ? 'GraphQL request' : $name; + $this->locationOffset = $location ?? new SourceLocation(1, 1); Utils::invariant( $this->locationOffset->line > 0, diff --git a/src/Language/Token.php b/src/Language/Token.php index 831b3adcf..1618103de 100644 --- a/src/Language/Token.php +++ b/src/Language/Token.php @@ -11,28 +11,28 @@ class Token { // Each kind of token. - const SOF = ''; - const EOF = ''; - const BANG = '!'; - const DOLLAR = '$'; - const AMP = '&'; - const PAREN_L = '('; - const PAREN_R = ')'; - const SPREAD = '...'; - const COLON = ':'; - const EQUALS = '='; - const AT = '@'; - const BRACKET_L = '['; - const BRACKET_R = ']'; - const BRACE_L = '{'; - const PIPE = '|'; - const BRACE_R = '}'; - const NAME = 'Name'; - const INT = 'Int'; - const FLOAT = 'Float'; - const STRING = 'String'; - const BLOCK_STRING = 'BlockString'; - const COMMENT = 'Comment'; + public const SOF = ''; + public const EOF = ''; + public const BANG = '!'; + public const DOLLAR = '$'; + public const AMP = '&'; + public const PAREN_L = '('; + public const PAREN_R = ')'; + public const SPREAD = '...'; + public const COLON = ':'; + public const EQUALS = '='; + public const AT = '@'; + public const BRACKET_L = '['; + public const BRACKET_R = ']'; + public const BRACE_L = '{'; + public const PIPE = '|'; + public const BRACE_R = '}'; + public const NAME = 'Name'; + public const INT = 'Int'; + public const FLOAT = 'Float'; + public const STRING = 'String'; + public const BLOCK_STRING = 'BlockString'; + public const COMMENT = 'Comment'; /** * The kind of Token (see one of constants above). @@ -81,18 +81,13 @@ class Token */ public $prev; - /** @var Token */ + /** @var Token|null */ public $next; /** - * @param string $kind - * @param int $start - * @param int $end - * @param int $line - * @param int $column - * @param mixed|null $value + * @param mixed $value */ - public function __construct($kind, $start, $end, $line, $column, ?Token $previous = null, $value = null) + public function __construct(string $kind, int $start, int $end, int $line, int $column, ?Token $previous = null, $value = null) { $this->kind = $kind; $this->start = $start; @@ -104,18 +99,15 @@ public function __construct($kind, $start, $end, $line, $column, ?Token $previou $this->value = $value; } - /** - * @return string - */ - public function getDescription() + public function getDescription() : string { - return $this->kind . ($this->value ? ' "' . $this->value . '"' : ''); + return $this->kind . ($this->value === null ? '' : ' "' . $this->value . '"'); } /** * @return (string|int|null)[] */ - public function toArray() + public function toArray() : array { return [ 'kind' => $this->kind, diff --git a/src/Language/Visitor.php b/src/Language/Visitor.php index 45f8fff13..95031e8ab 100644 --- a/src/Language/Visitor.php +++ b/src/Language/Visitor.php @@ -14,8 +14,6 @@ use stdClass; use function array_pop; use function array_splice; -use function call_user_func; -use function call_user_func_array; use function count; use function func_get_args; use function is_array; @@ -116,7 +114,7 @@ class Visitor NodeKind::NAME => [], NodeKind::DOCUMENT => ['definitions'], NodeKind::OPERATION_DEFINITION => ['name', 'variableDefinitions', 'directives', 'selectionSet'], - NodeKind::VARIABLE_DEFINITION => ['variable', 'type', 'defaultValue'], + NodeKind::VARIABLE_DEFINITION => ['variable', 'type', 'defaultValue', 'directives'], NodeKind::VARIABLE => ['name'], NodeKind::SELECTION_SET => ['selections'], NodeKind::FIELD => ['alias', 'name', 'arguments', 'directives', 'selectionSet'], @@ -153,7 +151,7 @@ class Visitor NodeKind::OBJECT_TYPE_DEFINITION => ['description', 'name', 'interfaces', 'directives', 'fields'], NodeKind::FIELD_DEFINITION => ['description', 'name', 'arguments', 'type', 'directives'], NodeKind::INPUT_VALUE_DEFINITION => ['description', 'name', 'type', 'defaultValue', 'directives'], - NodeKind::INTERFACE_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'], + NodeKind::INTERFACE_TYPE_DEFINITION => ['description', 'name', 'interfaces', 'directives', 'fields'], NodeKind::UNION_TYPE_DEFINITION => ['description', 'name', 'directives', 'types'], NodeKind::ENUM_TYPE_DEFINITION => ['description', 'name', 'directives', 'values'], NodeKind::ENUM_VALUE_DEFINITION => ['description', 'name', 'directives'], @@ -161,7 +159,7 @@ class Visitor NodeKind::SCALAR_TYPE_EXTENSION => ['name', 'directives'], NodeKind::OBJECT_TYPE_EXTENSION => ['name', 'interfaces', 'directives', 'fields'], - NodeKind::INTERFACE_TYPE_EXTENSION => ['name', 'directives', 'fields'], + NodeKind::INTERFACE_TYPE_EXTENSION => ['name', 'interfaces', 'directives', 'fields'], NodeKind::UNION_TYPE_EXTENSION => ['name', 'directives', 'types'], NodeKind::ENUM_TYPE_EXTENSION => ['name', 'directives', 'values'], NodeKind::INPUT_OBJECT_TYPE_EXTENSION => ['name', 'directives', 'fields'], @@ -186,7 +184,7 @@ class Visitor */ public static function visit($root, $visitor, $keyMap = null) { - $visitorKeys = $keyMap ?: self::$visitorKeys; + $visitorKeys = $keyMap ?? self::$visitorKeys; $stack = null; $inArray = $root instanceof NodeList || is_array($root); @@ -205,7 +203,7 @@ public static function visit($root, $visitor, $keyMap = null) $isLeaving = $index === count($keys); $key = null; $node = null; - $isEdited = $isLeaving && count($edits) !== 0; + $isEdited = $isLeaving && count($edits) > 0; if ($isLeaving) { $key = ! $ancestors ? $UNDEFINED : $path[count($path) - 1]; @@ -230,11 +228,7 @@ public static function visit($root, $visitor, $keyMap = null) $editKey -= $editOffset; } if ($inArray && $editValue === null) { - if ($node instanceof NodeList) { - $node->splice($editKey, 1); - } else { - array_splice($node, $editKey, 1); - } + $node->splice($editKey, 1); $editOffset++; } else { if ($node instanceof NodeList || is_array($node)) { @@ -251,8 +245,18 @@ public static function visit($root, $visitor, $keyMap = null) $inArray = $stack['inArray']; $stack = $stack['prev']; } else { - $key = $parent !== null ? ($inArray ? $index : $keys[$index]) : $UNDEFINED; - $node = $parent !== null ? ($parent instanceof NodeList || is_array($parent) ? $parent[$key] : $parent->{$key}) : $newRoot; + $key = $parent !== null + ? ($inArray + ? $index + : $keys[$index] + ) + : $UNDEFINED; + $node = $parent !== null + ? ($parent instanceof NodeList || is_array($parent) + ? $parent[$key] + : $parent->{$key} + ) + : $newRoot; if ($node === null || $node === $UNDEFINED) { continue; } @@ -269,8 +273,8 @@ public static function visit($root, $visitor, $keyMap = null) $visitFn = self::getVisitFn($visitor, $node->kind, $isLeaving); - if ($visitFn) { - $result = call_user_func($visitFn, $node, $key, $parent, $path, $ancestors); + if ($visitFn !== null) { + $result = $visitFn($node, $key, $parent, $path, $ancestors); $editValue = null; if ($result !== null) { @@ -318,7 +322,7 @@ public static function visit($root, $visitor, $keyMap = null) ]; $inArray = $node instanceof NodeList || is_array($node); - $keys = ($inArray ? $node : $visitorKeys[$node->kind]) ?: []; + $keys = ($inArray ? $node : $visitorKeys[$node->kind]) ?? []; $index = -1; $edits = []; if ($parent !== null) { @@ -328,7 +332,7 @@ public static function visit($root, $visitor, $keyMap = null) } } while ($stack); - if (count($edits) !== 0) { + if (count($edits) > 0) { $newRoot = $edits[0][1]; } @@ -383,7 +387,7 @@ public static function removeNode() /** * @param callable[][] $visitors * - * @return callable[][] + * @return array */ public static function visitInParallel($visitors) { @@ -393,7 +397,7 @@ public static function visitInParallel($visitors) return [ 'enter' => static function (Node $node) use ($visitors, $skipping, $visitorsCount) { for ($i = 0; $i < $visitorsCount; $i++) { - if (! empty($skipping[$i])) { + if ($skipping[$i] !== null) { continue; } @@ -407,7 +411,7 @@ public static function visitInParallel($visitors) continue; } - $result = call_user_func_array($fn, func_get_args()); + $result = $fn(...func_get_args()); if ($result instanceof VisitorOperation) { if ($result->doContinue) { @@ -424,15 +428,15 @@ public static function visitInParallel($visitors) }, 'leave' => static function (Node $node) use ($visitors, $skipping, $visitorsCount) { for ($i = 0; $i < $visitorsCount; $i++) { - if (empty($skipping[$i])) { + if ($skipping[$i] === null) { $fn = self::getVisitFn( $visitors[$i], $node->kind, /* isLeaving */ true ); - if ($fn) { - $result = call_user_func_array($fn, func_get_args()); + if (isset($fn)) { + $result = $fn(...func_get_args()); if ($result instanceof VisitorOperation) { if ($result->doBreak) { $skipping[$i] = $result; @@ -462,8 +466,8 @@ public static function visitWithTypeInfo(TypeInfo $typeInfo, $visitor) $typeInfo->enter($node); $fn = self::getVisitFn($visitor, $node->kind, false); - if ($fn) { - $result = call_user_func_array($fn, func_get_args()); + if (isset($fn)) { + $result = $fn(...func_get_args()); if ($result !== null) { $typeInfo->leave($node); if ($result instanceof Node) { @@ -478,7 +482,10 @@ public static function visitWithTypeInfo(TypeInfo $typeInfo, $visitor) }, 'leave' => static function (Node $node) use ($typeInfo, $visitor) { $fn = self::getVisitFn($visitor, $node->kind, true); - $result = $fn ? call_user_func_array($fn, func_get_args()) : null; + $result = $fn !== null + ? $fn(...func_get_args()) + : null; + $typeInfo->leave($node); return $result; @@ -490,10 +497,8 @@ public static function visitWithTypeInfo(TypeInfo $typeInfo, $visitor) * @param callable[]|null $visitor * @param string $kind * @param bool $isLeaving - * - * @return callable|null */ - public static function getVisitFn($visitor, $kind, $isLeaving) + public static function getVisitFn($visitor, $kind, $isLeaving) : ?callable { if ($visitor === null) { return null; @@ -501,11 +506,6 @@ public static function getVisitFn($visitor, $kind, $isLeaving) $kindVisitor = $visitor[$kind] ?? null; - if (! $isLeaving && is_callable($kindVisitor)) { - // { Kind() {} } - return $kindVisitor; - } - if (is_array($kindVisitor)) { if ($isLeaving) { $kindSpecificVisitor = $kindVisitor['leave'] ?? null; @@ -513,29 +513,24 @@ public static function getVisitFn($visitor, $kind, $isLeaving) $kindSpecificVisitor = $kindVisitor['enter'] ?? null; } - if ($kindSpecificVisitor && is_callable($kindSpecificVisitor)) { - // { Kind: { enter() {}, leave() {} } } - return $kindSpecificVisitor; - } + return $kindSpecificVisitor; + } - return null; + if ($kindVisitor !== null && ! $isLeaving) { + return $kindVisitor; } $visitor += ['leave' => null, 'enter' => null]; $specificVisitor = $isLeaving ? $visitor['leave'] : $visitor['enter']; - if ($specificVisitor) { - if (is_callable($specificVisitor)) { + if (isset($specificVisitor)) { + if (! is_array($specificVisitor)) { // { enter() {}, leave() {} } return $specificVisitor; } - $specificKindVisitor = $specificVisitor[$kind] ?? null; - if (is_callable($specificKindVisitor)) { - // { enter: { Kind() {} }, leave: { Kind() {} } } - return $specificKindVisitor; - } + return $specificVisitor[$kind] ?? null; } return null; diff --git a/src/Schema.php b/src/Schema.php deleted file mode 100644 index 351047ac2..000000000 --- a/src/Schema.php +++ /dev/null @@ -1,22 +0,0 @@ -readRawBody(); - $bodyParams = ['query' => $rawBody ?: '']; + $rawBody = $readRawBodyFn + ? $readRawBodyFn() + : $this->readRawBody(); + $bodyParams = ['query' => $rawBody ?? '']; } elseif (stripos($contentType, 'application/json') !== false) { - $rawBody = $readRawBodyFn ? $readRawBodyFn() : $this->readRawBody(); - $bodyParams = json_decode($rawBody ?: '', true); + $rawBody = $readRawBodyFn ? + $readRawBodyFn() + : $this->readRawBody(); + $bodyParams = json_decode($rawBody ?? '', true); if (json_last_error()) { throw new RequestError('Could not parse JSON: ' . json_last_error_msg()); @@ -142,7 +151,7 @@ public function parseRequestParams($method, array $bodyParams, array $queryParam * Checks validity of OperationParams extracted from HTTP request and returns an array of errors * if params are invalid (or empty array when params are valid) * - * @return Error[] + * @return array * * @api */ @@ -157,21 +166,21 @@ public function validateOperationParams(OperationParams $params) $errors[] = new RequestError('GraphQL Request parameters "query" and "queryId" are mutually exclusive'); } - if ($params->query !== null && (! is_string($params->query) || empty($params->query))) { + if ($params->query !== null && ! is_string($params->query)) { $errors[] = new RequestError( 'GraphQL Request parameter "query" must be string, but got ' . Utils::printSafeJson($params->query) ); } - if ($params->queryId !== null && (! is_string($params->queryId) || empty($params->queryId))) { + if ($params->queryId !== null && ! is_string($params->queryId)) { $errors[] = new RequestError( 'GraphQL Request parameter "queryId" must be string, but got ' . Utils::printSafeJson($params->queryId) ); } - if ($params->operation !== null && (! is_string($params->operation) || empty($params->operation))) { + if ($params->operation !== null && ! is_string($params->operation)) { $errors[] = new RequestError( 'GraphQL Request parameter "operation" must be string, but got ' . Utils::printSafeJson($params->operation) @@ -198,7 +207,7 @@ public function validateOperationParams(OperationParams $params) */ public function executeOperation(ServerConfig $config, OperationParams $op) { - $promiseAdapter = $config->getPromiseAdapter() ?: Executor::getPromiseAdapter(); + $promiseAdapter = $config->getPromiseAdapter() ?? Executor::getPromiseAdapter(); $result = $this->promiseToExecuteOperation($promiseAdapter, $config, $op); if ($promiseAdapter instanceof SyncPromiseAdapter) { @@ -220,7 +229,7 @@ public function executeOperation(ServerConfig $config, OperationParams $op) */ public function executeBatch(ServerConfig $config, array $operations) { - $promiseAdapter = $config->getPromiseAdapter() ?: Executor::getPromiseAdapter(); + $promiseAdapter = $config->getPromiseAdapter() ?? Executor::getPromiseAdapter(); $result = []; foreach ($operations as $operation) { @@ -249,7 +258,7 @@ private function promiseToExecuteOperation( $isBatch = false ) { try { - if (! $config->getSchema()) { + if ($config->getSchema() === null) { throw new InvariantViolation('Schema is required for the server'); } @@ -259,10 +268,10 @@ private function promiseToExecuteOperation( $errors = $this->validateOperationParams($op); - if (! empty($errors)) { + if (count($errors) > 0) { $errors = Utils::map( $errors, - static function (RequestError $err) { + static function (RequestError $err) : Error { return Error::createLocatedError($err, null, null); } ); @@ -272,13 +281,20 @@ static function (RequestError $err) { ); } - $doc = $op->queryId ? $this->loadPersistedQuery($config, $op) : $op->query; + $doc = $op->queryId + ? $this->loadPersistedQuery($config, $op) + : $op->query; if (! $doc instanceof DocumentNode) { $doc = Parser::parse($doc); } $operationType = AST::getOperation($doc, $op->operation); + + if ($operationType === false) { + throw new RequestError('Failed to determine operation type'); + } + if ($operationType !== 'query' && $op->isReadOnly()) { throw new RequestError('GET supports only query operation'); } @@ -304,15 +320,15 @@ static function (RequestError $err) { ); } - $applyErrorHandling = static function (ExecutionResult $result) use ($config) { + $applyErrorHandling = static function (ExecutionResult $result) use ($config) : ExecutionResult { if ($config->getErrorsHandler()) { $result->setErrorsHandler($config->getErrorsHandler()); } - if ($config->getErrorFormatter() || $config->getDebug()) { + if ($config->getErrorFormatter() || $config->getDebugFlag() !== DebugFlag::NONE) { $result->setErrorFormatter( FormattedError::prepareFormatter( $config->getErrorFormatter(), - $config->getDebug() + $config->getDebugFlag() ) ); } @@ -333,7 +349,7 @@ private function loadPersistedQuery(ServerConfig $config, OperationParams $opera // Load query if we got persisted query id: $loader = $config->getPersistentQueryLoader(); - if (! $loader) { + if ($loader === null) { throw new RequestError('Persisted queries are not supported by this server'); } @@ -379,19 +395,17 @@ private function resolveValidationRules( } /** - * @param string $operationType - * * @return mixed */ - private function resolveRootValue(ServerConfig $config, OperationParams $params, DocumentNode $doc, $operationType) + private function resolveRootValue(ServerConfig $config, OperationParams $params, DocumentNode $doc, string $operationType) { - $root = $config->getRootValue(); + $rootValue = $config->getRootValue(); - if (is_callable($root)) { - $root = $root($params, $doc, $operationType); + if (is_callable($rootValue)) { + $rootValue = $rootValue($params, $doc, $operationType); } - return $root; + return $rootValue; } /** @@ -425,7 +439,7 @@ private function resolveContextValue( public function sendResponse($result, $exitWhenDone = false) { if ($result instanceof Promise) { - $result->then(function ($actualResult) use ($exitWhenDone) { + $result->then(function ($actualResult) use ($exitWhenDone) : void { $this->doSendResponse($actualResult, $exitWhenDone); }); } else { @@ -473,7 +487,7 @@ private function resolveHttpStatus($result) if (is_array($result) && isset($result[0])) { Utils::each( $result, - static function ($executionResult, $index) { + static function ($executionResult, $index) : void { if (! $executionResult instanceof ExecutionResult) { throw new InvariantViolation(sprintf( 'Expecting every entry of batched query result to be instance of %s but entry at position %d is %s', @@ -493,7 +507,7 @@ static function ($executionResult, $index) { Utils::printSafe($result) )); } - if ($result->data === null && ! empty($result->errors)) { + if ($result->data === null && count($result->errors) > 0) { $httpStatus = 400; } else { $httpStatus = 200; @@ -512,7 +526,7 @@ static function ($executionResult, $index) { * * @api */ - public function parsePsrRequest(ServerRequestInterface $request) + public function parsePsrRequest(RequestInterface $request) { if ($request->getMethod() === 'GET') { $bodyParams = []; @@ -524,13 +538,17 @@ public function parsePsrRequest(ServerRequestInterface $request) } if (stripos($contentType[0], 'application/graphql') !== false) { - $bodyParams = ['query' => $request->getBody()->getContents()]; + $bodyParams = ['query' => (string) $request->getBody()]; } elseif (stripos($contentType[0], 'application/json') !== false) { - $bodyParams = $request->getParsedBody(); + $bodyParams = $request instanceof ServerRequestInterface + ? $request->getParsedBody() + : json_decode((string) $request->getBody(), true); if ($bodyParams === null) { throw new InvariantViolation( - 'PSR-7 request is expected to provide parsed body for "application/json" requests but got null' + $request instanceof ServerRequestInterface + ? 'Expected to receive a parsed body for "application/json" PSR-7 request but got null' + : 'Expected to receive a JSON array in body for "application/json" PSR-7 request' ); } @@ -541,7 +559,7 @@ public function parsePsrRequest(ServerRequestInterface $request) ); } } else { - $bodyParams = $request->getParsedBody(); + parse_str((string) $request->getBody(), $bodyParams); if (! is_array($bodyParams)) { throw new RequestError('Unexpected content type: ' . Utils::printSafeJson($contentType[0])); @@ -549,10 +567,12 @@ public function parsePsrRequest(ServerRequestInterface $request) } } + parse_str(html_entity_decode($request->getUri()->getQuery()), $queryParams); + return $this->parseRequestParams( $request->getMethod(), $bodyParams, - $request->getQueryParams() + $queryParams ); } diff --git a/src/Server/OperationParams.php b/src/Server/OperationParams.php index 3ab12de88..5490ca41b 100644 --- a/src/Server/OperationParams.php +++ b/src/Server/OperationParams.php @@ -8,7 +8,9 @@ use function is_string; use function json_decode; use function json_last_error; +use function strlen; use const CASE_LOWER; +use const JSON_ERROR_NONE; /** * Structure representing parsed HTTP parameters for GraphQL operation @@ -93,7 +95,7 @@ public static function create(array $params, bool $readonly = false) : Operation } $tmp = json_decode($params[$param], true); - if (json_last_error()) { + if (json_last_error() !== JSON_ERROR_NONE) { continue; } @@ -101,14 +103,14 @@ public static function create(array $params, bool $readonly = false) : Operation } $instance->query = $params['query']; - $instance->queryId = $params['queryid'] ?: $params['documentid'] ?: $params['id']; + $instance->queryId = $params['queryid'] ?? $params['documentid'] ?? $params['id']; $instance->operation = $params['operationname']; $instance->variables = $params['variables']; $instance->extensions = $params['extensions']; $instance->readOnly = $readonly; // Apollo server/client compatibility: look for the queryid in extensions - if (isset($instance->extensions['persistedQuery']['sha256Hash']) && empty($instance->query) && empty($instance->queryId)) { + if (isset($instance->extensions['persistedQuery']['sha256Hash']) && strlen($instance->query ?? '') === 0 && strlen($instance->queryId ?? '') === 0) { $instance->queryId = $instance->extensions['persistedQuery']['sha256Hash']; } diff --git a/src/Server/ServerConfig.php b/src/Server/ServerConfig.php index cb36cebd1..2c44dea4f 100644 --- a/src/Server/ServerConfig.php +++ b/src/Server/ServerConfig.php @@ -4,6 +4,7 @@ namespace GraphQL\Server; +use GraphQL\Error\DebugFlag; use GraphQL\Error\InvariantViolation; use GraphQL\Executor\Promise\PromiseAdapter; use GraphQL\Type\Schema; @@ -54,7 +55,7 @@ public static function create(array $config = []) return $instance; } - /** @var Schema */ + /** @var Schema|null */ private $schema; /** @var mixed|callable */ @@ -69,22 +70,22 @@ public static function create(array $config = []) /** @var callable|null */ private $errorsHandler; - /** @var bool */ - private $debug = false; + /** @var int */ + private $debugFlag = DebugFlag::NONE; /** @var bool */ private $queryBatching = false; - /** @var ValidationRule[]|callable */ + /** @var ValidationRule[]|callable|null */ private $validationRules; - /** @var callable */ + /** @var callable|null */ private $fieldResolver; - /** @var PromiseAdapter */ + /** @var PromiseAdapter|null */ private $promiseAdapter; - /** @var callable */ + /** @var callable|null */ private $persistentQueryLoader; /** @@ -158,7 +159,7 @@ public function setErrorsHandler(callable $handler) /** * Set validation rules for this server. * - * @param ValidationRule[]|callable $validationRules + * @param ValidationRule[]|callable|null $validationRules * * @return self * @@ -207,17 +208,13 @@ public function setPersistentQueryLoader(callable $persistentQueryLoader) } /** - * Set response debug flags. See GraphQL\Error\Debug class for a list of all available flags - * - * @param bool|int $set - * - * @return self + * Set response debug flags. @see \GraphQL\Error\DebugFlag class for a list of all available flags * * @api */ - public function setDebug($set = true) + public function setDebugFlag(int $debugFlag = DebugFlag::INCLUDE_DEBUG_MESSAGE) : self { - $this->debug = $set; + $this->debugFlag = $debugFlag; return $this; } @@ -263,7 +260,7 @@ public function getRootValue() } /** - * @return Schema + * @return Schema|null */ public function getSchema() { @@ -287,7 +284,7 @@ public function getErrorsHandler() } /** - * @return PromiseAdapter + * @return PromiseAdapter|null */ public function getPromiseAdapter() { @@ -295,7 +292,7 @@ public function getPromiseAdapter() } /** - * @return ValidationRule[]|callable + * @return ValidationRule[]|callable|null */ public function getValidationRules() { @@ -303,7 +300,7 @@ public function getValidationRules() } /** - * @return callable + * @return callable|null */ public function getFieldResolver() { @@ -311,19 +308,16 @@ public function getFieldResolver() } /** - * @return callable + * @return callable|null */ public function getPersistentQueryLoader() { return $this->persistentQueryLoader; } - /** - * @return bool - */ - public function getDebug() + public function getDebugFlag() : int { - return $this->debug; + return $this->debugFlag; } /** diff --git a/src/Server/StandardServer.php b/src/Server/StandardServer.php index 7b7e627b2..bb128ea9b 100644 --- a/src/Server/StandardServer.php +++ b/src/Server/StandardServer.php @@ -4,13 +4,14 @@ namespace GraphQL\Server; +use GraphQL\Error\DebugFlag; use GraphQL\Error\FormattedError; use GraphQL\Error\InvariantViolation; use GraphQL\Executor\ExecutionResult; use GraphQL\Executor\Promise\Promise; use GraphQL\Utils\Utils; +use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; use Throwable; use function is_array; @@ -50,12 +51,12 @@ class StandardServer * (e.g. during schema instantiation). * * @param Throwable $error - * @param bool $debug + * @param int $debug * @param bool $exitWhenDone * * @api */ - public static function send500Error($error, $debug = false, $exitWhenDone = false) + public static function send500Error($error, $debug = DebugFlag::NONE, $exitWhenDone = false) { $response = [ 'errors' => [FormattedError::createFromException($error, $debug)], @@ -146,7 +147,7 @@ public function executeRequest($parsedBody = null) * @api */ public function processPsrRequest( - ServerRequestInterface $request, + RequestInterface $request, ResponseInterface $response, StreamInterface $writableBodyStream ) { @@ -163,7 +164,7 @@ public function processPsrRequest( * * @api */ - public function executePsrRequest(ServerRequestInterface $request) + public function executePsrRequest(RequestInterface $request) { $parsedBody = $this->helper->parsePsrRequest($request); diff --git a/src/Type/Definition/AbstractType.php b/src/Type/Definition/AbstractType.php index 356dceb76..e02571549 100644 --- a/src/Type/Definition/AbstractType.php +++ b/src/Type/Definition/AbstractType.php @@ -4,12 +4,11 @@ namespace GraphQL\Type\Definition; -/* -export type GraphQLAbstractType = -GraphQLInterfaceType | -GraphQLUnionType; -*/ - +/** +export type AbstractType = +InterfaceType | +UnionType; + */ interface AbstractType { /** diff --git a/src/Type/Definition/BooleanType.php b/src/Type/Definition/BooleanType.php index 56b5f0fed..46dd943eb 100644 --- a/src/Type/Definition/BooleanType.php +++ b/src/Type/Definition/BooleanType.php @@ -20,11 +20,14 @@ class BooleanType extends ScalarType public $description = 'The `Boolean` scalar type represents `true` or `false`.'; /** - * @param mixed $value + * Serialize the given value to a boolean. * - * @return bool + * The GraphQL spec leaves this up to the implementations, so we just do what + * PHP does natively to make this intuitive for developers. + * + * @param mixed $value */ - public function serialize($value) + public function serialize($value) : bool { return (bool) $value; } @@ -42,22 +45,19 @@ public function parseValue($value) return $value; } - throw new Error('Cannot represent value as boolean: ' . Utils::printSafe($value)); + throw new Error('Boolean cannot represent a non boolean value: ' . Utils::printSafe($value)); } /** - * @param Node $valueNode * @param mixed[]|null $variables * - * @return bool|null - * * @throws Exception */ - public function parseLiteral($valueNode, ?array $variables = null) + public function parseLiteral(Node $valueNode, ?array $variables = null) { if (! $valueNode instanceof BooleanValueNode) { // Intentionally without message, as all information already in wrapped Exception - throw new Exception(); + throw new Error(); } return $valueNode->value; diff --git a/src/Type/Definition/CustomScalarType.php b/src/Type/Definition/CustomScalarType.php index 50b28ceb2..8a825306f 100644 --- a/src/Type/Definition/CustomScalarType.php +++ b/src/Type/Definition/CustomScalarType.php @@ -8,7 +8,6 @@ use GraphQL\Language\AST\Node; use GraphQL\Utils\AST; use GraphQL\Utils\Utils; -use function call_user_func; use function is_callable; use function sprintf; @@ -21,7 +20,7 @@ class CustomScalarType extends ScalarType */ public function serialize($value) { - return call_user_func($this->config['serialize'], $value); + return $this->config['serialize']($value); } /** @@ -32,26 +31,23 @@ public function serialize($value) public function parseValue($value) { if (isset($this->config['parseValue'])) { - return call_user_func($this->config['parseValue'], $value); + return $this->config['parseValue']($value); } return $value; } /** - * @param Node $valueNode * @param mixed[]|null $variables * * @return mixed * * @throws Exception */ - public function parseLiteral(/* GraphQL\Language\AST\ValueNode */ - $valueNode, - ?array $variables = null - ) { + public function parseLiteral(Node $valueNode, ?array $variables = null) + { if (isset($this->config['parseLiteral'])) { - return call_user_func($this->config['parseLiteral'], $valueNode, $variables); + return $this->config['parseLiteral']($valueNode, $variables); } return AST::valueFromASTUntyped($valueNode, $variables); diff --git a/src/Type/Definition/Directive.php b/src/Type/Definition/Directive.php index 0ffe496ae..10878e99c 100644 --- a/src/Type/Definition/Directive.php +++ b/src/Type/Definition/Directive.php @@ -4,25 +4,23 @@ namespace GraphQL\Type\Definition; +use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\DirectiveDefinitionNode; use GraphQL\Language\DirectiveLocation; -use GraphQL\Utils\Utils; use function array_key_exists; -use function array_keys; -use function in_array; use function is_array; class Directive { public const DEFAULT_DEPRECATION_REASON = 'No longer supported'; - const INCLUDE_NAME = 'include'; - const IF_ARGUMENT_NAME = 'if'; - const SKIP_NAME = 'skip'; - const DEPRECATED_NAME = 'deprecated'; - const REASON_ARGUMENT_NAME = 'reason'; + public const INCLUDE_NAME = 'include'; + public const IF_ARGUMENT_NAME = 'if'; + public const SKIP_NAME = 'skip'; + public const DEPRECATED_NAME = 'deprecated'; + public const REASON_ARGUMENT_NAME = 'reason'; - /** @var Directive[] */ + /** @var Directive[]|null */ public static $internalDirectives; // Schema Definitions @@ -33,12 +31,15 @@ class Directive /** @var string|null */ public $description; - /** @var string[] */ - public $locations; - /** @var FieldArgument[] */ public $args = []; + /** @var bool */ + public $isRepeatable; + + /** @var string[] */ + public $locations; + /** @var DirectiveDefinitionNode|null */ public $astNode; @@ -50,6 +51,13 @@ class Directive */ public function __construct(array $config) { + if (! isset($config['name'])) { + throw new InvariantViolation('Directive must be named.'); + } + $this->name = $config['name']; + + $this->description = $config['description'] ?? null; + if (isset($config['args'])) { $args = []; foreach ($config['args'] as $name => $arg) { @@ -60,14 +68,16 @@ public function __construct(array $config) } } $this->args = $args; - unset($config['args']); } - foreach ($config as $key => $value) { - $this->{$key} = $value; + + if (! isset($config['locations']) || ! is_array($config['locations'])) { + throw new InvariantViolation('Must provide locations for directive.'); } + $this->locations = $config['locations']; + + $this->isRepeatable = $config['isRepeatable'] ?? false; + $this->astNode = $config['astNode'] ?? null; - Utils::invariant($this->name, 'Directive must be named.'); - Utils::invariant(is_array($this->locations), 'Must provide locations for directive.'); $this->config = $config; } @@ -84,9 +94,9 @@ public static function includeDirective() /** * @return Directive[] */ - public static function getInternalDirectives() + public static function getInternalDirectives() : array { - if (! self::$internalDirectives) { + if (self::$internalDirectives === null) { self::$internalDirectives = [ 'include' => new self([ 'name' => self::INCLUDE_NAME, @@ -130,8 +140,8 @@ public static function getInternalDirectives() 'type' => Type::string(), 'description' => 'Explains why this element was deprecated, usually also including a ' . - 'suggestion for how to access supported similar data. Formatted ' . - 'in [Markdown](https://daringfireball.net/projects/markdown/).', + 'suggestion for how to access supported similar data. Formatted using ' . + 'the Markdown syntax (as specified by [CommonMark](https://commonmark.org/).', 'defaultValue' => self::DEFAULT_DEPRECATION_REASON, ]), ], diff --git a/src/Type/Definition/EnumType.php b/src/Type/Definition/EnumType.php index dc83d6907..9451ec74a 100644 --- a/src/Type/Definition/EnumType.php +++ b/src/Type/Definition/EnumType.php @@ -24,10 +24,20 @@ class EnumType extends Type implements InputType, OutputType, LeafType, Nullable /** @var EnumTypeDefinitionNode|null */ public $astNode; - /** @var EnumValueDefinition[] */ + /** + * Lazily initialized. + * + * @var EnumValueDefinition[] + */ private $values; - /** @var MixedStore */ + /** + * Lazily initialized. + * + * Actually a MixedStore, PHPStan won't let us type it that way. + * + * @var MixedStore + */ private $valueLookup; /** @var ArrayObject */ @@ -67,12 +77,10 @@ public function getValue($name) return $lookup[$name] ?? null; } - /** - * @return ArrayObject - */ - private function getNameLookup() + private function getNameLookup() : ArrayObject { if (! $this->nameLookup) { + /** @var ArrayObject $lookup */ $lookup = new ArrayObject(); foreach ($this->getValues() as $value) { $lookup[$value->name] = $value; @@ -86,9 +94,9 @@ private function getNameLookup() /** * @return EnumValueDefinition[] */ - public function getValues() + public function getValues() : array { - if ($this->values === null) { + if (! isset($this->values)) { $this->values = []; $config = $this->config; @@ -139,11 +147,11 @@ public function serialize($value) } /** - * @return MixedStore + * Actually returns a MixedStore, PHPStan won't let us type it that way */ - private function getValueLookup() + private function getValueLookup() : MixedStore { - if ($this->valueLookup === null) { + if (! isset($this->valueLookup)) { $this->valueLookup = new MixedStore(); foreach ($this->getValues() as $valueName => $value) { @@ -172,14 +180,13 @@ public function parseValue($value) } /** - * @param Node $valueNode * @param mixed[]|null $variables * * @return null * * @throws Exception */ - public function parseLiteral($valueNode, ?array $variables = null) + public function parseLiteral(Node $valueNode, ?array $variables = null) { if ($valueNode instanceof EnumValueNode) { $lookup = $this->getNameLookup(); @@ -192,7 +199,7 @@ public function parseLiteral($valueNode, ?array $variables = null) } // Intentionally without message, as all information already in wrapped Exception - throw new Exception(); + throw new Error(); } /** diff --git a/src/Type/Definition/FieldArgument.php b/src/Type/Definition/FieldArgument.php index 3bffe57d7..f6a8f3852 100644 --- a/src/Type/Definition/FieldArgument.php +++ b/src/Type/Definition/FieldArgument.php @@ -6,7 +6,9 @@ use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\InputValueDefinitionNode; +use GraphQL\Type\Schema; use GraphQL\Utils\Utils; +use function array_key_exists; use function is_array; use function is_string; use function sprintf; @@ -28,28 +30,19 @@ class FieldArgument /** @var mixed[] */ public $config; - /** @var InputType */ + /** @var Type&InputType */ private $type; - /** @var bool */ - private $defaultValueExists = false; - - /** - * @param mixed[] $def - */ + /** @param mixed[] $def */ public function __construct(array $def) { foreach ($def as $key => $value) { switch ($key) { - case 'type': - $this->type = $value; - break; case 'name': $this->name = $value; break; case 'defaultValue': - $this->defaultValue = $value; - $this->defaultValueExists = true; + $this->defaultValue = $value; break; case 'description': $this->description = $value; @@ -67,7 +60,7 @@ public function __construct(array $def) * * @return FieldArgument[] */ - public static function createMap(array $config) + public static function createMap(array $config) : array { $map = []; foreach ($config as $name => $argConfig) { @@ -80,20 +73,29 @@ public static function createMap(array $config) return $map; } - /** - * @return InputType - */ - public function getType() + public function getType() : Type { + if (! isset($this->type)) { + /** + * TODO: replace this phpstan cast with native assert + * + * @var Type&InputType + */ + $type = Schema::resolveType($this->config['type']); + $this->type = $type; + } + return $this->type; } - /** - * @return bool - */ - public function defaultValueExists() + public function defaultValueExists() : bool + { + return array_key_exists('defaultValue', $this->config); + } + + public function isRequired() : bool { - return $this->defaultValueExists; + return $this->getType() instanceof NonNull && ! $this->defaultValueExists(); } public function assertValid(FieldDefinition $parentField, Type $parentType) @@ -105,7 +107,7 @@ public function assertValid(FieldDefinition $parentField, Type $parentType) sprintf('%s.%s(%s:) %s', $parentType->name, $parentField->name, $this->name, $e->getMessage()) ); } - $type = $this->type; + $type = $this->getType(); if ($type instanceof WrappingType) { $type = $type->getWrappedType(true); } diff --git a/src/Type/Definition/FieldDefinition.php b/src/Type/Definition/FieldDefinition.php index 8f409bf8c..baf18402b 100644 --- a/src/Type/Definition/FieldDefinition.php +++ b/src/Type/Definition/FieldDefinition.php @@ -6,7 +6,9 @@ use GraphQL\Error\Error; use GraphQL\Error\InvariantViolation; +use GraphQL\Error\Warning; use GraphQL\Language\AST\FieldDefinitionNode; +use GraphQL\Type\Schema; use GraphQL\Utils\Utils; use function is_array; use function is_callable; @@ -30,7 +32,7 @@ class FieldDefinition * Callback for resolving field value given parent value. * Mutually exclusive with `map` * - * @var callable + * @var callable|null */ public $resolveFn; @@ -38,7 +40,7 @@ class FieldDefinition * Callback for mapping list of parent values to list of field values. * Mutually exclusive with `resolve` * - * @var callable + * @var callable|null */ public $mapFn; @@ -58,8 +60,8 @@ class FieldDefinition */ public $config; - /** @var OutputType */ - public $type; + /** @var OutputType&Type */ + private $type; /** @var callable|string */ private $complexityFn; @@ -70,7 +72,6 @@ class FieldDefinition protected function __construct(array $config) { $this->name = $config['name']; - $this->type = $config['type']; $this->resolveFn = $config['resolve'] ?? null; $this->mapFn = $config['map'] ?? null; $this->args = isset($config['args']) ? FieldArgument::createMap($config['args']) : []; @@ -84,7 +85,12 @@ protected function __construct(array $config) $this->complexityFn = $config['complexity'] ?? self::DEFAULT_COMPLEXITY_FN; } - public static function defineFieldMap(Type $type, $fields) + /** + * @param (callable():mixed[])|mixed[] $fields + * + * @return array + */ + public static function defineFieldMap(Type $type, $fields) : array { if (is_callable($fields)) { $fields = $fields(); @@ -164,7 +170,7 @@ public static function defaultComplexity($childrenComplexity) */ public function getArg($name) { - foreach ($this->args ?: [] as $arg) { + foreach ($this->args ?? [] as $arg) { /** @var FieldArgument $arg */ if ($arg->name === $name) { return $arg; @@ -174,14 +180,73 @@ public function getArg($name) return null; } - /** - * @return Type - */ - public function getType() + public function getType() : Type { + if (! isset($this->type)) { + /** + * TODO: replace this phpstan cast with native assert + * + * @var Type&OutputType + */ + $type = Schema::resolveType($this->config['type']); + $this->type = $type; + } + return $this->type; } + public function __isset(string $name) : bool + { + switch ($name) { + case 'type': + Warning::warnOnce( + "The public getter for 'type' on FieldDefinition has been deprecated and will be removed" . + " in the next major version. Please update your code to use the 'getType' method.", + Warning::WARNING_CONFIG_DEPRECATION + ); + + return isset($this->type); + } + + return isset($this->$name); + } + + public function __get(string $name) + { + switch ($name) { + case 'type': + Warning::warnOnce( + "The public getter for 'type' on FieldDefinition has been deprecated and will be removed" . + " in the next major version. Please update your code to use the 'getType' method.", + Warning::WARNING_CONFIG_DEPRECATION + ); + + return $this->getType(); + default: + return $this->$name; + } + + return null; + } + + public function __set(string $name, $value) + { + switch ($name) { + case 'type': + Warning::warnOnce( + "The public setter for 'type' on FieldDefinition has been deprecated and will be removed" . + ' in the next major version.', + Warning::WARNING_CONFIG_DEPRECATION + ); + $this->type = $value; + break; + + default: + $this->$name = $value; + break; + } + } + /** * @return bool */ @@ -217,7 +282,7 @@ public function assertValid(Type $parentType) ) ); - $type = $this->type; + $type = $this->getType(); if ($type instanceof WrappingType) { $type = $type->getWrappedType(true); } @@ -239,5 +304,9 @@ public function assertValid(Type $parentType) Utils::printSafe($this->resolveFn) ) ); + + foreach ($this->args as $fieldArgument) { + $fieldArgument->assertValid($this, $type); + } } } diff --git a/src/Type/Definition/FloatType.php b/src/Type/Definition/FloatType.php index e8923ca6f..4ce723cf1 100644 --- a/src/Type/Definition/FloatType.php +++ b/src/Type/Definition/FloatType.php @@ -10,6 +10,11 @@ use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\Node; use GraphQL\Utils\Utils; +use function floatval; +use function is_bool; +use function is_finite; +use function is_float; +use function is_int; use function is_numeric; class FloatType extends ScalarType @@ -26,60 +31,59 @@ class FloatType extends ScalarType /** * @param mixed $value * - * @return float|null - * * @throws Error */ - public function serialize($value) - { - return $this->coerceFloat($value); - } - - private function coerceFloat($value) + public function serialize($value) : float { - if ($value === '') { - throw new Error( - 'Float cannot represent non numeric value: (empty string)' - ); - } + $float = is_numeric($value) || is_bool($value) + ? (float) $value + : null; - if (! is_numeric($value) && $value !== true && $value !== false) { + if ($float === null || ! is_finite($float)) { throw new Error( 'Float cannot represent non numeric value: ' . Utils::printSafe($value) ); } - return (float) $value; + return $float; } /** * @param mixed $value * - * @return float|null - * * @throws Error */ - public function parseValue($value) + public function parseValue($value) : float { - return $this->coerceFloat($value); + $float = is_float($value) || is_int($value) + ? (float) $value + : null; + + if ($float === null || ! is_finite($float)) { + throw new Error( + 'Float cannot represent non numeric value: ' . + Utils::printSafe($value) + ); + } + + return $float; } /** - * @param Node $valueNode * @param mixed[]|null $variables * - * @return float|null + * @return float * * @throws Exception */ - public function parseLiteral($valueNode, ?array $variables = null) + public function parseLiteral(Node $valueNode, ?array $variables = null) { if ($valueNode instanceof FloatValueNode || $valueNode instanceof IntValueNode) { return (float) $valueNode->value; } // Intentionally without message, as all information already in wrapped Exception - throw new Exception(); + throw new Error(); } } diff --git a/src/Type/Definition/IDType.php b/src/Type/Definition/IDType.php index 807739367..d5a214d97 100644 --- a/src/Type/Definition/IDType.php +++ b/src/Type/Definition/IDType.php @@ -12,7 +12,6 @@ use GraphQL\Utils\Utils; use function is_int; use function is_object; -use function is_scalar; use function is_string; use function method_exists; @@ -38,17 +37,12 @@ class IDType extends ScalarType */ public function serialize($value) { - if ($value === true) { - return 'true'; - } - if ($value === false) { - return 'false'; - } - if ($value === null) { - return 'null'; - } - if (! is_scalar($value) && (! is_object($value) || ! method_exists($value, '__toString'))) { - throw new Error('ID type cannot represent non scalar value: ' . Utils::printSafe($value)); + $canCast = is_string($value) + || is_int($value) + || (is_object($value) && method_exists($value, '__toString')); + + if (! $canCast) { + throw new Error('ID cannot represent value: ' . Utils::printSafe($value)); } return (string) $value; @@ -57,34 +51,30 @@ public function serialize($value) /** * @param mixed $value * - * @return string - * * @throws Error */ - public function parseValue($value) + public function parseValue($value) : string { if (is_string($value) || is_int($value)) { return (string) $value; } - - throw new Error('Cannot represent value as ID: ' . Utils::printSafe($value)); + throw new Error('ID cannot represent value: ' . Utils::printSafe($value)); } /** - * @param Node $valueNode * @param mixed[]|null $variables * - * @return string|null + * @return string * * @throws Exception */ - public function parseLiteral($valueNode, ?array $variables = null) + public function parseLiteral(Node $valueNode, ?array $variables = null) { if ($valueNode instanceof StringValueNode || $valueNode instanceof IntValueNode) { return $valueNode->value; } // Intentionally without message, as all information already in wrapped Exception - throw new Exception(); + throw new Error(); } } diff --git a/src/Type/Definition/ImplementingType.php b/src/Type/Definition/ImplementingType.php new file mode 100644 index 000000000..94bf77179 --- /dev/null +++ b/src/Type/Definition/ImplementingType.php @@ -0,0 +1,20 @@ + + */ + public function getInterfaces() : array; +} diff --git a/src/Type/Definition/InputObjectField.php b/src/Type/Definition/InputObjectField.php index 82fa5704d..e2578ef62 100644 --- a/src/Type/Definition/InputObjectField.php +++ b/src/Type/Definition/InputObjectField.php @@ -6,8 +6,11 @@ use GraphQL\Error\Error; use GraphQL\Error\InvariantViolation; +use GraphQL\Error\Warning; use GraphQL\Language\AST\InputValueDefinitionNode; +use GraphQL\Type\Schema; use GraphQL\Utils\Utils; +use function array_key_exists; use function sprintf; class InputObjectField @@ -21,8 +24,8 @@ class InputObjectField /** @var string|null */ public $description; - /** @var mixed */ - public $type; + /** @var Type&InputType */ + private $type; /** @var InputValueDefinitionNode|null */ public $astNode; @@ -30,13 +33,6 @@ class InputObjectField /** @var mixed[] */ public $config; - /** - * Helps to differentiate when `defaultValue` is `null` and when it was not even set initially - * - * @var bool - */ - private $defaultValueExists = false; - /** * @param mixed[] $opts */ @@ -45,11 +41,13 @@ public function __construct(array $opts) foreach ($opts as $k => $v) { switch ($k) { case 'defaultValue': - $this->defaultValue = $v; - $this->defaultValueExists = true; + $this->defaultValue = $v; break; case 'defaultValueExists': break; + case 'type': + // do nothing; type is lazy loaded in getType + break; default: $this->{$k} = $v; } @@ -57,20 +55,84 @@ public function __construct(array $opts) $this->config = $opts; } + public function __isset(string $name) : bool + { + switch ($name) { + case 'type': + Warning::warnOnce( + "The public getter for 'type' on InputObjectField has been deprecated and will be removed" . + " in the next major version. Please update your code to use the 'getType' method.", + Warning::WARNING_CONFIG_DEPRECATION + ); + + return isset($this->type); + } + + return isset($this->$name); + } + + public function __get(string $name) + { + switch ($name) { + case 'type': + Warning::warnOnce( + "The public getter for 'type' on InputObjectField has been deprecated and will be removed" . + " in the next major version. Please update your code to use the 'getType' method.", + Warning::WARNING_CONFIG_DEPRECATION + ); + + return $this->getType(); + default: + return $this->$name; + } + + return null; + } + + public function __set(string $name, $value) + { + switch ($name) { + case 'type': + Warning::warnOnce( + "The public setter for 'type' on InputObjectField has been deprecated and will be removed" . + ' in the next major version.', + Warning::WARNING_CONFIG_DEPRECATION + ); + $this->type = $value; + break; + + default: + $this->$name = $value; + break; + } + } + /** - * @return mixed + * @return Type&InputType */ - public function getType() + public function getType() : Type { + if (! isset($this->type)) { + /** + * TODO: replace this phpstan cast with native assert + * + * @var Type&InputType + */ + $type = Schema::resolveType($this->config['type']); + $this->type = $type; + } + return $this->type; } - /** - * @return bool - */ - public function defaultValueExists() + public function defaultValueExists() : bool + { + return array_key_exists('defaultValue', $this->config); + } + + public function isRequired() : bool { - return $this->defaultValueExists; + return $this->getType() instanceof NonNull && ! $this->defaultValueExists(); } /** @@ -83,7 +145,7 @@ public function assertValid(Type $parentType) } catch (Error $e) { throw new InvariantViolation(sprintf('%s.%s: %s', $parentType->name, $this->name, $e->getMessage())); } - $type = $this->type; + $type = $this->getType(); if ($type instanceof WrappingType) { $type = $type->getWrappedType(true); } @@ -97,9 +159,9 @@ public function assertValid(Type $parentType) ) ); Utils::invariant( - empty($this->config['resolve']), + ! array_key_exists('resolve', $this->config), sprintf( - '%s.%s field type has a resolve property, but Input Types cannot define resolvers.', + '%s.%s field has a resolve property, but Input Types cannot define resolvers.', $parentType->name, $this->name ) diff --git a/src/Type/Definition/InputObjectType.php b/src/Type/Definition/InputObjectType.php index c921dda00..b55755d44 100644 --- a/src/Type/Definition/InputObjectType.php +++ b/src/Type/Definition/InputObjectType.php @@ -4,12 +4,11 @@ namespace GraphQL\Type\Definition; -use Exception; use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\InputObjectTypeDefinitionNode; use GraphQL\Language\AST\InputObjectTypeExtensionNode; use GraphQL\Utils\Utils; -use function call_user_func; +use function count; use function is_array; use function is_callable; use function is_string; @@ -20,7 +19,11 @@ class InputObjectType extends Type implements InputType, NullableType, NamedType /** @var InputObjectTypeDefinitionNode|null */ public $astNode; - /** @var InputObjectField[] */ + /** + * Lazily initialized. + * + * @var InputObjectField[] + */ private $fields; /** @var InputObjectTypeExtensionNode[] */ @@ -45,16 +48,12 @@ public function __construct(array $config) } /** - * @param string $name - * - * @return InputObjectField - * - * @throws Exception + * @throws InvariantViolation */ - public function getField($name) + public function getField(string $name) : InputObjectField { - if ($this->fields === null) { - $this->getFields(); + if (! isset($this->fields)) { + $this->initializeFields(); } Utils::invariant(isset($this->fields[$name]), "Field '%s' is not defined for type '%s'", $name, $this->name); @@ -64,43 +63,50 @@ public function getField($name) /** * @return InputObjectField[] */ - public function getFields() + public function getFields() : array { - if ($this->fields === null) { - $this->fields = []; - $fields = $this->config['fields'] ?? []; - $fields = is_callable($fields) ? call_user_func($fields) : $fields; - - if (! is_array($fields)) { - throw new InvariantViolation( - sprintf('%s fields must be an array or a callable which returns such an array.', $this->name) - ); - } - - foreach ($fields as $name => $field) { - if ($field instanceof Type) { - $field = ['type' => $field]; - } - $field = new InputObjectField($field + ['name' => $name]); - $this->fields[$field->name] = $field; - } + if (! isset($this->fields)) { + $this->initializeFields(); } return $this->fields; } + protected function initializeFields() : void + { + $this->fields = []; + $fields = $this->config['fields'] ?? []; + if (is_callable($fields)) { + $fields = $fields(); + } + + if (! is_array($fields)) { + throw new InvariantViolation( + sprintf('%s fields must be an array or a callable which returns such an array.', $this->name) + ); + } + + foreach ($fields as $name => $field) { + if ($field instanceof Type || is_callable($field)) { + $field = ['type' => $field]; + } + $field = new InputObjectField($field + ['name' => $name]); + $this->fields[$field->name] = $field; + } + } + /** * Validates type config and throws if one of type options is invalid. * Note: this method is shallow, it won't validate object fields and their arguments. * * @throws InvariantViolation */ - public function assertValid() + public function assertValid() : void { parent::assertValid(); Utils::invariant( - ! empty($this->getFields()), + count($this->getFields()) > 0, sprintf( '%s fields must be an associative array with field names as keys or a callable which returns such an array.', $this->name diff --git a/src/Type/Definition/InputType.php b/src/Type/Definition/InputType.php index 043765753..d970bff3d 100644 --- a/src/Type/Definition/InputType.php +++ b/src/Type/Definition/InputType.php @@ -4,20 +4,19 @@ namespace GraphQL\Type\Definition; -/* -export type GraphQLInputType = - | GraphQLScalarType - | GraphQLEnumType - | GraphQLInputObjectType - | GraphQLList - | GraphQLNonNull< - | GraphQLScalarType - | GraphQLEnumType - | GraphQLInputObjectType - | GraphQLList, +/** +export type InputType = + | ScalarType + | EnumType + | InputObjectType + | ListOfType + | NonNull< + | ScalarType + | EnumType + | InputObjectType + | ListOfType, >; */ - interface InputType { } diff --git a/src/Type/Definition/IntType.php b/src/Type/Definition/IntType.php index e6176c33b..e2180df1a 100644 --- a/src/Type/Definition/IntType.php +++ b/src/Type/Definition/IntType.php @@ -10,8 +10,11 @@ use GraphQL\Language\AST\Node; use GraphQL\Utils\Utils; use function floatval; +use function floor; use function intval; use function is_bool; +use function is_float; +use function is_int; use function is_numeric; class IntType extends ScalarType @@ -41,63 +44,66 @@ class IntType extends ScalarType */ public function serialize($value) { - return $this->coerceInt($value); - } - - /** - * @param mixed $value - * - * @return int - */ - private function coerceInt($value) - { - if ($value === '') { - throw new Error( - 'Int cannot represent non 32-bit signed integer value: (empty string)' - ); + // Fast path for 90+% of cases: + if (is_int($value) && $value <= self::MAX_INT && $value >= self::MIN_INT) { + return $value; } - $num = floatval($value); - if ((! is_numeric($value) && ! is_bool($value)) || $num > self::MAX_INT || $num < self::MIN_INT) { + $float = is_numeric($value) || is_bool($value) + ? (float) $value + : null; + + if ($float === null || floor($float) !== $float) { throw new Error( - 'Int cannot represent non 32-bit signed integer value: ' . + 'Int cannot represent non-integer value: ' . Utils::printSafe($value) ); } - $int = intval($num); - // int cast with == used for performance reasons - // phpcs:ignore - if ($int != $num) { + + if ($float > self::MAX_INT || $float < self::MIN_INT) { throw new Error( - 'Int cannot represent non-integer value: ' . + 'Int cannot represent non 32-bit signed integer value: ' . Utils::printSafe($value) ); } - return $int; + return (int) $float; } /** * @param mixed $value * - * @return int|null - * * @throws Error */ - public function parseValue($value) + public function parseValue($value) : int { - return $this->coerceInt($value); + $isInt = is_int($value) || (is_float($value) && floor($value) === $value); + + if (! $isInt) { + throw new Error( + 'Int cannot represent non-integer value: ' . + Utils::printSafe($value) + ); + } + + if ($value > self::MAX_INT || $value < self::MIN_INT) { + throw new Error( + 'Int cannot represent non 32-bit signed integer value: ' . + Utils::printSafe($value) + ); + } + + return (int) $value; } /** - * @param Node $valueNode * @param mixed[]|null $variables * - * @return int|null + * @return int * * @throws Exception */ - public function parseLiteral($valueNode, ?array $variables = null) + public function parseLiteral(Node $valueNode, ?array $variables = null) { if ($valueNode instanceof IntValueNode) { $val = (int) $valueNode->value; @@ -107,6 +113,6 @@ public function parseLiteral($valueNode, ?array $variables = null) } // Intentionally without message, as all information already in wrapped Exception - throw new Exception(); + throw new Error(); } } diff --git a/src/Type/Definition/InterfaceType.php b/src/Type/Definition/InterfaceType.php index c13d7fb05..845f04d0c 100644 --- a/src/Type/Definition/InterfaceType.php +++ b/src/Type/Definition/InterfaceType.php @@ -7,22 +7,43 @@ use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\InterfaceTypeDefinitionNode; use GraphQL\Language\AST\InterfaceTypeExtensionNode; +use GraphQL\Type\Schema; use GraphQL\Utils\Utils; +use function array_map; +use function is_array; use function is_callable; use function is_string; use function sprintf; -class InterfaceType extends Type implements AbstractType, OutputType, CompositeType, NullableType, NamedType +class InterfaceType extends Type implements AbstractType, OutputType, CompositeType, NullableType, NamedType, ImplementingType { /** @var InterfaceTypeDefinitionNode|null */ public $astNode; - /** @var InterfaceTypeExtensionNode[] */ + /** @var array */ public $extensionASTNodes; - /** @var FieldDefinition[] */ + /** + * Lazily initialized. + * + * @var array + */ private $fields; + /** + * Lazily initialized. + * + * @var array + */ + private $interfaces; + + /** + * Lazily initialized. + * + * @var array + */ + private $interfaceMap; + /** * @param mixed[] $config */ @@ -44,9 +65,11 @@ public function __construct(array $config) /** * @param mixed $type * - * @return self + * @return $this + * + * @throws InvariantViolation */ - public static function assertInterfaceType($type) + public static function assertInterfaceType($type) : self { Utils::invariant( $type instanceof self, @@ -56,30 +79,20 @@ public static function assertInterfaceType($type) return $type; } - /** - * @param string $name - * - * @return FieldDefinition - */ - public function getField($name) + public function getField(string $name) : FieldDefinition { - if ($this->fields === null) { - $this->getFields(); + if (! isset($this->fields)) { + $this->initializeFields(); } Utils::invariant(isset($this->fields[$name]), 'Field "%s" is not defined for type "%s"', $name, $this->name); return $this->fields[$name]; } - /** - * @param string $name - * - * @return bool - */ - public function hasField($name) + public function hasField(string $name) : bool { - if ($this->fields === null) { - $this->getFields(); + if (! isset($this->fields)) { + $this->initializeFields(); } return isset($this->fields[$name]); @@ -88,21 +101,68 @@ public function hasField($name) /** * @return FieldDefinition[] */ - public function getFields() + public function getFields() : array { - if ($this->fields === null) { - $fields = $this->config['fields'] ?? []; - $this->fields = FieldDefinition::defineFieldMap($this, $fields); + if (! isset($this->fields)) { + $this->initializeFields(); } return $this->fields; } + protected function initializeFields() : void + { + $fields = $this->config['fields'] ?? []; + $this->fields = FieldDefinition::defineFieldMap($this, $fields); + } + + public function implementsInterface(InterfaceType $interfaceType) : bool + { + if (! isset($this->interfaceMap)) { + $this->interfaceMap = []; + foreach ($this->getInterfaces() as $interface) { + /** @var Type&InterfaceType $interface */ + $interface = Schema::resolveType($interface); + $this->interfaceMap[$interface->name] = $interface; + } + } + + return isset($this->interfaceMap[$interfaceType->name]); + } + + /** + * @return array + */ + public function getInterfaces() : array + { + if (! isset($this->interfaces)) { + $interfaces = $this->config['interfaces'] ?? []; + if (is_callable($interfaces)) { + $interfaces = $interfaces(); + } + + if ($interfaces !== null && ! is_array($interfaces)) { + throw new InvariantViolation( + sprintf('%s interfaces must be an Array or a callable which returns an Array.', $this->name) + ); + } + + /** @var array $interfaces */ + $interfaces = $interfaces === null + ? [] + : array_map([Schema::class, 'resolveType'], $interfaces); + + $this->interfaces = $interfaces; + } + + return $this->interfaces; + } + /** * Resolves concrete ObjectType for given object value * - * @param object $objectValue - * @param mixed[] $context + * @param object $objectValue + * @param mixed $context * * @return Type|null */ @@ -120,7 +180,7 @@ public function resolveType($objectValue, $context, ResolveInfo $info) /** * @throws InvariantViolation */ - public function assertValid() + public function assertValid() : void { parent::assertValid(); diff --git a/src/Type/Definition/LeafType.php b/src/Type/Definition/LeafType.php index ece73554b..b913823f8 100644 --- a/src/Type/Definition/LeafType.php +++ b/src/Type/Definition/LeafType.php @@ -6,7 +6,12 @@ use Exception; use GraphQL\Error\Error; +use GraphQL\Language\AST\BooleanValueNode; +use GraphQL\Language\AST\FloatValueNode; +use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\Node; +use GraphQL\Language\AST\NullValueNode; +use GraphQL\Language\AST\StringValueNode; /* export type GraphQLLeafType = @@ -45,12 +50,12 @@ public function parseValue($value); * * In the case of an invalid node or value this method must throw an Exception * - * @param Node $valueNode - * @param mixed[]|null $variables + * @param IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|NullValueNode $valueNode + * @param mixed[]|null $variables * * @return mixed * * @throws Exception */ - public function parseLiteral($valueNode, ?array $variables = null); + public function parseLiteral(Node $valueNode, ?array $variables = null); } diff --git a/src/Type/Definition/ListOfType.php b/src/Type/Definition/ListOfType.php index eb8200d13..c771766f6 100644 --- a/src/Type/Definition/ListOfType.php +++ b/src/Type/Definition/ListOfType.php @@ -4,33 +4,38 @@ namespace GraphQL\Type\Definition; +use GraphQL\Type\Schema; +use function is_callable; + class ListOfType extends Type implements WrappingType, OutputType, NullableType, InputType { - /** @var ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType */ + /** @var callable():Type|Type */ public $ofType; /** - * @param callable|Type $type + * @param callable():Type|Type $type */ public function __construct($type) { - $this->ofType = Type::assertType($type); + $this->ofType = is_callable($type) ? $type : Type::assertType($type); } public function toString() : string { - return '[' . $this->ofType->toString() . ']'; + return '[' . $this->getOfType()->toString() . ']'; } - /** - * @param bool $recurse - * - * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType - */ - public function getWrappedType($recurse = false) + public function getOfType() + { + return Schema::resolveType($this->ofType); + } + + public function getWrappedType(bool $recurse = false) : Type { - $type = $this->ofType; + $type = $this->getOfType(); - return $recurse && $type instanceof WrappingType ? $type->getWrappedType($recurse) : $type; + return $recurse && $type instanceof WrappingType + ? $type->getWrappedType($recurse) + : $type; } } diff --git a/src/Type/Definition/NamedType.php b/src/Type/Definition/NamedType.php index 372bcc5ba..d36dff5ab 100644 --- a/src/Type/Definition/NamedType.php +++ b/src/Type/Definition/NamedType.php @@ -4,16 +4,15 @@ namespace GraphQL\Type\Definition; -/* -export type GraphQLNamedType = - | GraphQLScalarType - | GraphQLObjectType - | GraphQLInterfaceType - | GraphQLUnionType - | GraphQLEnumType - | GraphQLInputObjectType; +/** +export type NamedType = + | ScalarType + | ObjectType + | InterfaceType + | UnionType + | EnumType + | InputObjectType; */ - interface NamedType { } diff --git a/src/Type/Definition/NonNull.php b/src/Type/Definition/NonNull.php index 6799523b8..25ba1c903 100644 --- a/src/Type/Definition/NonNull.php +++ b/src/Type/Definition/NonNull.php @@ -4,68 +4,42 @@ namespace GraphQL\Type\Definition; -use GraphQL\Utils\Utils; +use GraphQL\Type\Schema; class NonNull extends Type implements WrappingType, OutputType, InputType { - /** @var NullableType */ + /** @var callable():(NullableType&Type)|(NullableType&Type) */ private $ofType; /** - * @param NullableType $type + * code sniffer doesn't understand this syntax. Pr with a fix here: waiting on https://github.com/squizlabs/PHP_CodeSniffer/pull/2919 + * phpcs:disable Squiz.Commenting.FunctionComment.SpacingAfterParamType + * @param callable():(NullableType&Type)|(NullableType&Type) $type */ public function __construct($type) { - $this->ofType = self::assertNullableType($type); + $this->ofType = $type; } - /** - * @param mixed $type - * - * @return NullableType - */ - public static function assertNullableType($type) + public function toString() : string { - Utils::invariant( - Type::isType($type) && ! $type instanceof self, - 'Expected ' . Utils::printSafe($type) . ' to be a GraphQL nullable type.' - ); - - return $type; - } - - /** - * @param mixed $type - * - * @return self - */ - public static function assertNullType($type) - { - Utils::invariant( - $type instanceof self, - 'Expected ' . Utils::printSafe($type) . ' to be a GraphQL Non-Null type.' - ); - - return $type; + return $this->getWrappedType()->toString() . '!'; } - /** - * @return string - */ - public function toString() + public function getOfType() { - return $this->getWrappedType()->toString() . '!'; + return Schema::resolveType($this->ofType); } /** - * @param bool $recurse - * - * @return Type + * @return (NullableType&Type) */ - public function getWrappedType($recurse = false) + public function getWrappedType(bool $recurse = false) : Type { - $type = $this->ofType; + $type = $this->getOfType(); - return $recurse && $type instanceof WrappingType ? $type->getWrappedType($recurse) : $type; + return $recurse && $type instanceof WrappingType + ? $type->getWrappedType($recurse) + : $type; } } diff --git a/src/Type/Definition/ObjectType.php b/src/Type/Definition/ObjectType.php index f5092936d..f5dfd339f 100644 --- a/src/Type/Definition/ObjectType.php +++ b/src/Type/Definition/ObjectType.php @@ -4,12 +4,13 @@ namespace GraphQL\Type\Definition; -use Exception; +use GraphQL\Deferred; use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\ObjectTypeDefinitionNode; use GraphQL\Language\AST\ObjectTypeExtensionNode; +use GraphQL\Type\Schema; use GraphQL\Utils\Utils; -use function call_user_func; +use function array_map; use function is_array; use function is_callable; use function is_string; @@ -54,7 +55,7 @@ * } * ]); */ -class ObjectType extends Type implements OutputType, CompositeType, NullableType, NamedType +class ObjectType extends Type implements OutputType, CompositeType, NullableType, NamedType, ImplementingType { /** @var ObjectTypeDefinitionNode|null */ public $astNode; @@ -62,16 +63,28 @@ class ObjectType extends Type implements OutputType, CompositeType, NullableType /** @var ObjectTypeExtensionNode[] */ public $extensionASTNodes; - /** @var callable */ + /** @var ?callable */ public $resolveFieldFn; - /** @var FieldDefinition[] */ + /** + * Lazily initialized. + * + * @var FieldDefinition[] + */ private $fields; - /** @var InterfaceType[] */ + /** + * Lazily initialized. + * + * @var array + */ private $interfaces; - /** @var InterfaceType[]|null */ + /** + * Lazily initialized. + * + * @var array + */ private $interfaceMap; /** @@ -96,9 +109,11 @@ public function __construct(array $config) /** * @param mixed $type * - * @return self + * @return $this + * + * @throws InvariantViolation */ - public static function assertObjectType($type) + public static function assertObjectType($type) : self { Utils::invariant( $type instanceof self, @@ -109,31 +124,22 @@ public static function assertObjectType($type) } /** - * @param string $name - * - * @return FieldDefinition - * - * @throws Exception + * @throws InvariantViolation */ - public function getField($name) + public function getField(string $name) : FieldDefinition { - if ($this->fields === null) { - $this->getFields(); + if (! isset($this->fields)) { + $this->initializeFields(); } Utils::invariant(isset($this->fields[$name]), 'Field "%s" is not defined for type "%s"', $name, $this->name); return $this->fields[$name]; } - /** - * @param string $name - * - * @return bool - */ - public function hasField($name) + public function hasField(string $name) : bool { - if ($this->fields === null) { - $this->getFields(); + if (! isset($this->fields)) { + $this->initializeFields(); } return isset($this->fields[$name]); @@ -144,48 +150,45 @@ public function hasField($name) * * @throws InvariantViolation */ - public function getFields() + public function getFields() : array { - if ($this->fields === null) { - $fields = $this->config['fields'] ?? []; - $this->fields = FieldDefinition::defineFieldMap($this, $fields); + if (! isset($this->fields)) { + $this->initializeFields(); } return $this->fields; } - /** - * @param InterfaceType $iface - * - * @return bool - */ - public function implementsInterface($iface) + protected function initializeFields() : void { - $map = $this->getInterfaceMap(); - - return isset($map[$iface->name]); + $fields = $this->config['fields'] ?? []; + $this->fields = FieldDefinition::defineFieldMap($this, $fields); } - private function getInterfaceMap() + public function implementsInterface(InterfaceType $interfaceType) : bool { - if (! $this->interfaceMap) { + if (! isset($this->interfaceMap)) { $this->interfaceMap = []; foreach ($this->getInterfaces() as $interface) { + /** @var Type&InterfaceType $interface */ + $interface = Schema::resolveType($interface); $this->interfaceMap[$interface->name] = $interface; } } - return $this->interfaceMap; + return isset($this->interfaceMap[$interfaceType->name]); } /** - * @return InterfaceType[] + * @return array */ - public function getInterfaces() + public function getInterfaces() : array { - if ($this->interfaces === null) { + if (! isset($this->interfaces)) { $interfaces = $this->config['interfaces'] ?? []; - $interfaces = is_callable($interfaces) ? call_user_func($interfaces) : $interfaces; + if (is_callable($interfaces)) { + $interfaces = $interfaces(); + } if ($interfaces !== null && ! is_array($interfaces)) { throw new InvariantViolation( @@ -193,26 +196,30 @@ public function getInterfaces() ); } - $this->interfaces = $interfaces ?: []; + /** @var InterfaceType[] $interfaces */ + $interfaces = array_map([Schema::class, 'resolveType'], $interfaces ?? []); + + $this->interfaces = $interfaces; } return $this->interfaces; } /** - * @param mixed[] $value - * @param mixed[]|null $context + * @param mixed $value + * @param mixed $context * - * @return bool|null + * @return bool|Deferred|null */ public function isTypeOf($value, $context, ResolveInfo $info) { - return isset($this->config['isTypeOf']) ? call_user_func( - $this->config['isTypeOf'], - $value, - $context, - $info - ) : null; + return isset($this->config['isTypeOf']) + ? $this->config['isTypeOf']( + $value, + $context, + $info + ) + : null; } /** @@ -221,7 +228,7 @@ public function isTypeOf($value, $context, ResolveInfo $info) * * @throws InvariantViolation */ - public function assertValid() + public function assertValid() : void { parent::assertValid(); diff --git a/src/Type/Definition/QueryPlan.php b/src/Type/Definition/QueryPlan.php index 235066d60..4483f4c31 100644 --- a/src/Type/Definition/QueryPlan.php +++ b/src/Type/Definition/QueryPlan.php @@ -12,7 +12,9 @@ use GraphQL\Language\AST\InlineFragmentNode; use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Type\Schema; +use function array_diff_key; use function array_filter; +use function array_intersect_key; use function array_key_exists; use function array_keys; use function array_merge; @@ -32,7 +34,7 @@ class QueryPlan /** @var Schema */ private $schema; - /** @var mixed[] */ + /** @var array */ private $queryPlan = []; /** @var mixed[] */ @@ -41,16 +43,21 @@ class QueryPlan /** @var FragmentDefinitionNode[] */ private $fragments; + /** @var bool */ + private $groupImplementorFields; + /** * @param FieldNode[] $fieldNodes * @param mixed[] $variableValues * @param FragmentDefinitionNode[] $fragments + * @param mixed[] $options */ - public function __construct(ObjectType $parentType, Schema $schema, iterable $fieldNodes, array $variableValues, array $fragments) + public function __construct(ObjectType $parentType, Schema $schema, iterable $fieldNodes, array $variableValues, array $fragments, array $options = []) { - $this->schema = $schema; - $this->variableValues = $variableValues; - $this->fragments = $fragments; + $this->schema = $schema; + $this->variableValues = $variableValues; + $this->fragments = $fragments; + $this->groupImplementorFields = in_array('group-implementor-fields', $options, true); $this->analyzeQueryPlan($parentType, $fieldNodes); } @@ -72,7 +79,7 @@ public function getReferencedTypes() : array public function hasType(string $type) : bool { - return count(array_filter($this->getReferencedTypes(), static function (string $referencedType) use ($type) { + return count(array_filter($this->getReferencedTypes(), static function (string $referencedType) use ($type) : bool { return $type === $referencedType; })) > 0; } @@ -87,7 +94,7 @@ public function getReferencedFields() : array public function hasField(string $field) : bool { - return count(array_filter($this->getReferencedFields(), static function (string $referencedField) use ($field) { + return count(array_filter($this->getReferencedFields(), static function (string $referencedField) use ($field) : bool { return $field === $referencedField; })) > 0; } @@ -109,7 +116,8 @@ public function subFields(string $typename) : array */ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) : void { - $queryPlan = []; + $queryPlan = []; + $implementors = []; /** @var FieldNode $fieldNode */ foreach ($fieldNodes as $fieldNode) { if (! $fieldNode->selectionSet) { @@ -118,10 +126,10 @@ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) $type = $parentType->getField($fieldNode->name->value)->getType(); if ($type instanceof WrappingType) { - $type = $type->getWrappedType(); + $type = $type->getWrappedType(true); } - $subfields = $this->analyzeSelectionSet($fieldNode->selectionSet, $type); + $subfields = $this->analyzeSelectionSet($fieldNode->selectionSet, $type, $implementors); $this->types[$type->name] = array_unique(array_merge( array_key_exists($type->name, $this->types) ? $this->types[$type->name] : [], @@ -134,26 +142,39 @@ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes) ); } - $this->queryPlan = $queryPlan; + if ($this->groupImplementorFields) { + $this->queryPlan = ['fields' => $queryPlan]; + + if ($implementors) { + $this->queryPlan['implementors'] = $implementors; + } + } else { + $this->queryPlan = $queryPlan; + } } /** + * @param InterfaceType|ObjectType $parentType + * @param mixed[] $implementors + * * @return mixed[] * * @throws Error */ - private function analyzeSelectionSet(SelectionSetNode $selectionSet, ObjectType $parentType) : array + private function analyzeSelectionSet(SelectionSetNode $selectionSet, Type $parentType, array &$implementors) : array { - $fields = []; + $fields = []; + $implementors = []; foreach ($selectionSet->selections as $selectionNode) { if ($selectionNode instanceof FieldNode) { $fieldName = $selectionNode->name->value; $type = $parentType->getField($fieldName); $selectionType = $type->getType(); - $subfields = []; + $subfields = []; + $subImplementors = []; if ($selectionNode->selectionSet) { - $subfields = $this->analyzeSubFields($selectionType, $selectionNode->selectionSet); + $subfields = $this->analyzeSubFields($selectionType, $selectionNode->selectionSet, $subImplementors); } $fields[$fieldName] = [ @@ -161,26 +182,21 @@ private function analyzeSelectionSet(SelectionSetNode $selectionSet, ObjectType 'fields' => $subfields ?? [], 'args' => Values::getArgumentValues($type, $selectionNode, $this->variableValues), ]; + if ($this->groupImplementorFields && $subImplementors) { + $fields[$fieldName]['implementors'] = $subImplementors; + } } elseif ($selectionNode instanceof FragmentSpreadNode) { $spreadName = $selectionNode->name->value; if (isset($this->fragments[$spreadName])) { $fragment = $this->fragments[$spreadName]; $type = $this->schema->getType($fragment->typeCondition->name->value); $subfields = $this->analyzeSubFields($type, $fragment->selectionSet); - - $fields = $this->arrayMergeDeep( - $subfields, - $fields - ); + $fields = $this->mergeFields($parentType, $type, $fields, $subfields, $implementors); } } elseif ($selectionNode instanceof InlineFragmentNode) { $type = $this->schema->getType($selectionNode->typeCondition->name->value); $subfields = $this->analyzeSubFields($type, $selectionNode->selectionSet); - - $fields = $this->arrayMergeDeep( - $subfields, - $fields - ); + $fields = $this->mergeFields($parentType, $type, $fields, $subfields, $implementors); } } @@ -188,17 +204,19 @@ private function analyzeSelectionSet(SelectionSetNode $selectionSet, ObjectType } /** + * @param mixed[] $implementors + * * @return mixed[] */ - private function analyzeSubFields(Type $type, SelectionSetNode $selectionSet) : array + private function analyzeSubFields(Type $type, SelectionSetNode $selectionSet, array &$implementors = []) : array { if ($type instanceof WrappingType) { - $type = $type->getWrappedType(); + $type = $type->getWrappedType(true); } $subfields = []; - if ($type instanceof ObjectType) { - $subfields = $this->analyzeSelectionSet($selectionSet, $type); + if ($type instanceof ObjectType || $type instanceof AbstractType) { + $subfields = $this->analyzeSelectionSet($selectionSet, $type, $implementors); $this->types[$type->name] = array_unique(array_merge( array_key_exists($type->name, $this->types) ? $this->types[$type->name] : [], array_keys($subfields) @@ -208,6 +226,38 @@ private function analyzeSubFields(Type $type, SelectionSetNode $selectionSet) : return $subfields; } + /** + * @param mixed[] $fields + * @param mixed[] $subfields + * @param mixed[] $implementors + * + * @return mixed[] + */ + private function mergeFields(Type $parentType, Type $type, array $fields, array $subfields, array &$implementors) : array + { + if ($this->groupImplementorFields && $parentType instanceof AbstractType && ! $type instanceof AbstractType) { + $implementors[$type->name] = [ + 'type' => $type, + 'fields' => $this->arrayMergeDeep( + $implementors[$type->name]['fields'] ?? [], + array_diff_key($subfields, $fields) + ), + ]; + + $fields = $this->arrayMergeDeep( + $fields, + array_intersect_key($subfields, $fields) + ); + } else { + $fields = $this->arrayMergeDeep( + $subfields, + $fields + ); + } + + return $fields; + } + /** * similar to array_merge_recursive this merges nested arrays, but handles non array values differently * while array_merge_recursive tries to merge non array values, in this implementation they will be overwritten diff --git a/src/Type/Definition/ResolveInfo.php b/src/Type/Definition/ResolveInfo.php index f2525dea5..6c118c234 100644 --- a/src/Type/Definition/ResolveInfo.php +++ b/src/Type/Definition/ResolveInfo.php @@ -15,12 +15,21 @@ /** * Structure containing information useful for field resolution process. - * Passed as 3rd argument to every field resolver. See [docs on field resolving (data fetching)](data-fetching.md). + * + * Passed as 4th argument to every field resolver. See [docs on field resolving (data fetching)](data-fetching.md). */ class ResolveInfo { /** - * The name of the field being resolved + * The definition of the field being resolved. + * + * @api + * @var FieldDefinition + */ + public $fieldDefinition; + + /** + * The name of the field being resolved. * * @api * @var string @@ -28,23 +37,23 @@ class ResolveInfo public $fieldName; /** - * AST of all nodes referencing this field in the query. + * Expected return type of the field being resolved. * * @api - * @var FieldNode[] + * @var Type */ - public $fieldNodes = []; + public $returnType; /** - * Expected return type of the field being resolved + * AST of all nodes referencing this field in the query. * * @api - * @var ScalarType|ObjectType|InterfaceType|UnionType|EnumType|ListOfType|NonNull + * @var FieldNode[] */ - public $returnType; + public $fieldNodes = []; /** - * Parent type of the field being resolved + * Parent type of the field being resolved. * * @api * @var ObjectType @@ -52,15 +61,15 @@ class ResolveInfo public $parentType; /** - * Path to this field from the very root value + * Path to this field from the very root value. * * @api - * @var string[][] + * @var string[] */ public $path; /** - * Instance of a schema used for execution + * Instance of a schema used for execution. * * @api * @var Schema @@ -68,7 +77,7 @@ class ResolveInfo public $schema; /** - * AST of all fragments defined in query + * AST of all fragments defined in query. * * @api * @var FragmentDefinitionNode[] @@ -76,15 +85,15 @@ class ResolveInfo public $fragments = []; /** - * Root value passed to query execution + * Root value passed to query execution. * * @api - * @var mixed|null + * @var mixed */ public $rootValue; /** - * AST of operation definition node (query, mutation) + * AST of operation definition node (query, mutation). * * @api * @var OperationDefinitionNode|null @@ -92,28 +101,30 @@ class ResolveInfo public $operation; /** - * Array of variables passed to query execution + * Array of variables passed to query execution. * * @api * @var mixed[] */ public $variableValues = []; - /** @var QueryPlan */ + /** + * Lazily initialized. + * + * @var QueryPlan + */ private $queryPlan; /** - * @param FieldNode[] $fieldNodes - * @param ScalarType|ObjectType|InterfaceType|UnionType|EnumType|ListOfType|NonNull $returnType - * @param string[][] $path - * @param FragmentDefinitionNode[] $fragments - * @param mixed|null $rootValue - * @param mixed[] $variableValues + * @param FieldNode[] $fieldNodes + * @param string[] $path + * @param FragmentDefinitionNode[] $fragments + * @param mixed|null $rootValue + * @param mixed[] $variableValues */ public function __construct( - string $fieldName, + FieldDefinition $fieldDefinition, iterable $fieldNodes, - $returnType, ObjectType $parentType, array $path, Schema $schema, @@ -122,21 +133,22 @@ public function __construct( ?OperationDefinitionNode $operation, array $variableValues ) { - $this->fieldName = $fieldName; - $this->fieldNodes = $fieldNodes; - $this->returnType = $returnType; - $this->parentType = $parentType; - $this->path = $path; - $this->schema = $schema; - $this->fragments = $fragments; - $this->rootValue = $rootValue; - $this->operation = $operation; - $this->variableValues = $variableValues; + $this->fieldDefinition = $fieldDefinition; + $this->fieldName = $fieldDefinition->name; + $this->returnType = $fieldDefinition->getType(); + $this->fieldNodes = $fieldNodes; + $this->parentType = $parentType; + $this->path = $path; + $this->schema = $schema; + $this->fragments = $fragments; + $this->rootValue = $rootValue; + $this->operation = $operation; + $this->variableValues = $variableValues; } /** * Helper method that returns names of all fields selected in query for - * $this->fieldName up to $depth levels + * $this->fieldName up to $depth levels. * * Example: * query MyQuery{ @@ -167,7 +179,7 @@ public function __construct( * * @param int $depth How many levels to include in output * - * @return bool[] + * @return array * * @api */ @@ -177,21 +189,32 @@ public function getFieldSelection($depth = 0) /** @var FieldNode $fieldNode */ foreach ($this->fieldNodes as $fieldNode) { - $fields = array_merge_recursive($fields, $this->foldSelectionSet($fieldNode->selectionSet, $depth)); + if ($fieldNode->selectionSet === null) { + continue; + } + + $fields = array_merge_recursive( + $fields, + $this->foldSelectionSet($fieldNode->selectionSet, $depth) + ); } return $fields; } - public function lookAhead() : QueryPlan + /** + * @param mixed[] $options + */ + public function lookAhead(array $options = []) : QueryPlan { - if ($this->queryPlan === null) { + if (! isset($this->queryPlan)) { $this->queryPlan = new QueryPlan( $this->parentType, $this->schema, $this->fieldNodes, $this->variableValues, - $this->fragments + $this->fragments, + $options ); } @@ -206,7 +229,7 @@ private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend) $fields = []; foreach ($selectionSet->selections as $selectionNode) { if ($selectionNode instanceof FieldNode) { - $fields[$selectionNode->name->value] = $descend > 0 && ! empty($selectionNode->selectionSet) + $fields[$selectionNode->name->value] = $descend > 0 && $selectionNode->selectionSet !== null ? $this->foldSelectionSet($selectionNode->selectionSet, $descend - 1) : true; } elseif ($selectionNode instanceof FragmentSpreadNode) { diff --git a/src/Type/Definition/StringType.php b/src/Type/Definition/StringType.php index 533298fc2..4a26b7853 100644 --- a/src/Type/Definition/StringType.php +++ b/src/Type/Definition/StringType.php @@ -9,9 +9,9 @@ use GraphQL\Language\AST\Node; use GraphQL\Language\AST\StringValueNode; use GraphQL\Utils\Utils; -use function is_array; use function is_object; use function is_scalar; +use function is_string; use function method_exists; class StringType extends ScalarType @@ -34,31 +34,13 @@ class StringType extends ScalarType */ public function serialize($value) { - if ($value === true) { - return 'true'; - } - if ($value === false) { - return 'false'; - } - if ($value === null) { - return 'null'; - } - if (is_object($value) && method_exists($value, '__toString')) { - return (string) $value; - } - if (! is_scalar($value)) { - throw new Error('String cannot represent non scalar value: ' . Utils::printSafe($value)); - } - - return $this->coerceString($value); - } + $canCast = is_scalar($value) + || (is_object($value) && method_exists($value, '__toString')) + || $value === null; - private function coerceString($value) - { - if (is_array($value)) { + if (! $canCast) { throw new Error( - 'String cannot represent an array value: ' . - Utils::printSafe($value) + 'String cannot represent value: ' . Utils::printSafe($value) ); } @@ -74,24 +56,29 @@ private function coerceString($value) */ public function parseValue($value) { - return $this->coerceString($value); + if (! is_string($value)) { + throw new Error( + 'String cannot represent a non string value: ' . Utils::printSafe($value) + ); + } + + return $value; } /** - * @param Node $valueNode * @param mixed[]|null $variables * - * @return string|null + * @return string * * @throws Exception */ - public function parseLiteral($valueNode, ?array $variables = null) + public function parseLiteral(Node $valueNode, ?array $variables = null) { if ($valueNode instanceof StringValueNode) { return $valueNode->value; } // Intentionally without message, as all information already in wrapped Exception - throw new Exception(); + throw new Error(); } } diff --git a/src/Type/Definition/Type.php b/src/Type/Definition/Type.php index 69faa1cc5..864bcfed2 100644 --- a/src/Type/Definition/Type.php +++ b/src/Type/Definition/Type.php @@ -4,7 +4,6 @@ namespace GraphQL\Type\Definition; -use Exception; use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\TypeDefinitionNode; use GraphQL\Language\AST\TypeExtensionNode; @@ -12,9 +11,9 @@ use GraphQL\Utils\Utils; use JsonSerializable; use ReflectionClass; -use Throwable; use function array_keys; use function array_merge; +use function assert; use function implode; use function in_array; use function preg_replace; @@ -33,8 +32,8 @@ abstract class Type implements JsonSerializable public const FLOAT = 'Float'; public const ID = 'ID'; - /** @var Type[] */ - private static $standardTypes; + /** @var array */ + protected static $standardTypes; /** @var Type[] */ private static $builtInTypes; @@ -55,105 +54,87 @@ abstract class Type implements JsonSerializable public $extensionASTNodes; /** - * @return IDType - * * @api */ - public static function id() + public static function id() : ScalarType { - return self::getStandardType(self::ID); - } - - /** - * @param string $name - * - * @return (IDType|StringType|FloatType|IntType|BooleanType)[]|IDType|StringType|FloatType|IntType|BooleanType - */ - private static function getStandardType($name = null) - { - if (self::$standardTypes === null) { - self::$standardTypes = [ - self::ID => new IDType(), - self::STRING => new StringType(), - self::FLOAT => new FloatType(), - self::INT => new IntType(), - self::BOOLEAN => new BooleanType(), - ]; + if (! isset(static::$standardTypes[self::ID])) { + static::$standardTypes[self::ID] = new IDType(); } - return $name ? self::$standardTypes[$name] : self::$standardTypes; + return static::$standardTypes[self::ID]; } /** - * @return StringType - * * @api */ - public static function string() + public static function string() : ScalarType { - return self::getStandardType(self::STRING); + if (! isset(static::$standardTypes[self::STRING])) { + static::$standardTypes[self::STRING] = new StringType(); + } + + return static::$standardTypes[self::STRING]; } /** - * @return BooleanType - * * @api */ - public static function boolean() + public static function boolean() : ScalarType { - return self::getStandardType(self::BOOLEAN); + if (! isset(static::$standardTypes[self::BOOLEAN])) { + static::$standardTypes[self::BOOLEAN] = new BooleanType(); + } + + return static::$standardTypes[self::BOOLEAN]; } /** - * @return IntType - * * @api */ - public static function int() + public static function int() : ScalarType { - return self::getStandardType(self::INT); + if (! isset(static::$standardTypes[self::INT])) { + static::$standardTypes[self::INT] = new IntType(); + } + + return static::$standardTypes[self::INT]; } /** - * @return FloatType - * * @api */ - public static function float() + public static function float() : ScalarType { - return self::getStandardType(self::FLOAT); + if (! isset(static::$standardTypes[self::FLOAT])) { + static::$standardTypes[self::FLOAT] = new FloatType(); + } + + return static::$standardTypes[self::FLOAT]; } /** - * @param Type|ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType|ListOfType|NonNull $wrappedType - * - * @return ListOfType - * * @api */ - public static function listOf($wrappedType) + public static function listOf(Type $wrappedType) : ListOfType { return new ListOfType($wrappedType); } /** - * @param NullableType $wrappedType - * - * @return NonNull + * @param callable|NullableType $wrappedType * * @api */ - public static function nonNull($wrappedType) + public static function nonNull($wrappedType) : NonNull { return new NonNull($wrappedType); } /** * Checks if the type is a builtin type - * - * @return bool */ - public static function isBuiltInType(Type $type) + public static function isBuiltInType(Type $type) : bool { return in_array($type->name, array_keys(self::getAllBuiltInTypes()), true); } @@ -179,17 +160,25 @@ public static function getAllBuiltInTypes() /** * Returns all builtin scalar types * - * @return Type[] + * @return ScalarType[] */ public static function getStandardTypes() { - return self::getStandardType(); + return [ + self::ID => static::id(), + self::STRING => static::string(), + self::FLOAT => static::float(), + self::INT => static::int(), + self::BOOLEAN => static::boolean(), + ]; } /** * @deprecated Use method getStandardTypes() instead * * @return Type[] + * + * @codeCoverageIgnore */ public static function getInternalTypes() { @@ -199,7 +188,7 @@ public static function getInternalTypes() } /** - * @param Type[] $types + * @param array $types */ public static function overrideStandardTypes(array $types) { @@ -217,35 +206,26 @@ public static function overrideStandardTypes(array $types) implode(', ', array_keys($standardTypes)), Utils::printSafe($type->name ?? null) ); - $standardTypes[$type->name] = $type; + static::$standardTypes[$type->name] = $type; } - self::$standardTypes = $standardTypes; } /** * @param Type $type * - * @return bool - * * @api */ - public static function isInputType($type) + public static function isInputType($type) : bool { - return $type instanceof InputType && - ( - ! $type instanceof WrappingType || - self::getNamedType($type) instanceof InputType - ); + return self::getNamedType($type) instanceof InputType; } /** * @param Type $type * - * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType - * * @api */ - public static function getNamedType($type) + public static function getNamedType($type) : ?Type { if ($type === null) { return null; @@ -260,27 +240,19 @@ public static function getNamedType($type) /** * @param Type $type * - * @return bool - * * @api */ - public static function isOutputType($type) + public static function isOutputType($type) : bool { - return $type instanceof OutputType && - ( - ! $type instanceof WrappingType || - self::getNamedType($type) instanceof OutputType - ); + return self::getNamedType($type) instanceof OutputType; } /** * @param Type $type * - * @return bool - * * @api */ - public static function isLeafType($type) + public static function isLeafType($type) : bool { return $type instanceof LeafType; } @@ -288,11 +260,9 @@ public static function isLeafType($type) /** * @param Type $type * - * @return bool - * * @api */ - public static function isCompositeType($type) + public static function isCompositeType($type) : bool { return $type instanceof CompositeType; } @@ -300,52 +270,31 @@ public static function isCompositeType($type) /** * @param Type $type * - * @return bool - * * @api */ - public static function isAbstractType($type) + public static function isAbstractType($type) : bool { return $type instanceof AbstractType; } /** * @param mixed $type - * - * @return mixed */ - public static function assertType($type) + public static function assertType($type) : Type { - Utils::invariant( - self::isType($type), - 'Expected ' . Utils::printSafe($type) . ' to be a GraphQL type.' - ); + assert($type instanceof Type, new InvariantViolation('Expected ' . Utils::printSafe($type) . ' to be a GraphQL type.')); return $type; } /** - * @param Type $type - * - * @return bool - * - * @api - */ - public static function isType($type) - { - return $type instanceof Type; - } - - /** - * @param Type $type - * - * @return NullableType - * * @api */ - public static function getNullableType($type) + public static function getNullableType(Type $type) : Type { - return $type instanceof NonNull ? $type->getWrappedType() : $type; + return $type instanceof NonNull + ? $type->getWrappedType() + : $type; } /** @@ -377,13 +326,7 @@ public function toString() */ public function __toString() { - try { - return $this->toString(); - } catch (Exception $e) { - echo $e; - } catch (Throwable $e) { - echo $e; - } + return $this->toString(); } /** diff --git a/src/Type/Definition/UnionType.php b/src/Type/Definition/UnionType.php index 3229df44b..6301e60a7 100644 --- a/src/Type/Definition/UnionType.php +++ b/src/Type/Definition/UnionType.php @@ -7,8 +7,8 @@ use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\UnionTypeDefinitionNode; use GraphQL\Language\AST\UnionTypeExtensionNode; +use GraphQL\Type\Schema; use GraphQL\Utils\Utils; -use function call_user_func; use function is_array; use function is_callable; use function is_string; @@ -19,16 +19,27 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType, /** @var UnionTypeDefinitionNode */ public $astNode; - /** @var ObjectType[] */ + /** + * Lazily initialized. + * + * @var ObjectType[] + */ private $types; - /** @var ObjectType[] */ + /** + * Lazily initialized. + * + * @var array + */ private $possibleTypeNames; /** @var UnionTypeExtensionNode[] */ public $extensionASTNodes; - public function __construct($config) + /** + * @param mixed[] $config + */ + public function __construct(array $config) { if (! isset($config['name'])) { $config['name'] = $this->tryInferName(); @@ -38,7 +49,7 @@ public function __construct($config) /** * Optionally provide a custom type resolver function. If one is not provided, - * the default implemenation will call `isTypeOf` on each implementing + * the default implementation will call `isTypeOf` on each implementing * Object type. */ $this->name = $config['name']; @@ -54,7 +65,7 @@ public function isPossibleType(Type $type) : bool return false; } - if ($this->possibleTypeNames === null) { + if (! isset($this->possibleTypeNames)) { $this->possibleTypeNames = []; foreach ($this->getTypes() as $possibleType) { $this->possibleTypeNames[$possibleType->name] = true; @@ -66,16 +77,15 @@ public function isPossibleType(Type $type) : bool /** * @return ObjectType[] + * + * @throws InvariantViolation */ - public function getTypes() + public function getTypes() : array { - if ($this->types === null) { - if (! isset($this->config['types'])) { - $types = null; - } elseif (is_callable($this->config['types'])) { - $types = call_user_func($this->config['types']); - } else { - $types = $this->config['types']; + if (! isset($this->types)) { + $types = $this->config['types'] ?? null; + if (is_callable($types)) { + $types = $types(); } if (! is_array($types)) { @@ -87,7 +97,12 @@ public function getTypes() ); } - $this->types = $types; + $rawTypes = $types; + foreach ($rawTypes as $i => $rawType) { + $rawTypes[$i] = Schema::resolveType($rawType); + } + + $this->types = $rawTypes; } return $this->types; @@ -115,7 +130,7 @@ public function resolveType($objectValue, $context, ResolveInfo $info) /** * @throws InvariantViolation */ - public function assertValid() + public function assertValid() : void { parent::assertValid(); diff --git a/src/Type/Definition/WrappingType.php b/src/Type/Definition/WrappingType.php index 8bff76805..26fbe85d3 100644 --- a/src/Type/Definition/WrappingType.php +++ b/src/Type/Definition/WrappingType.php @@ -6,10 +6,5 @@ interface WrappingType { - /** - * @param bool $recurse - * - * @return ObjectType|InterfaceType|UnionType|ScalarType|InputObjectType|EnumType - */ - public function getWrappedType($recurse = false); + public function getWrappedType(bool $recurse = false) : Type; } diff --git a/src/Type/Introspection.php b/src/Type/Introspection.php index f0cacee65..5f9599fa4 100644 --- a/src/Type/Introspection.php +++ b/src/Type/Introspection.php @@ -5,6 +5,7 @@ namespace GraphQL\Type; use Exception; +use GraphQL\GraphQL; use GraphQL\Language\DirectiveLocation; use GraphQL\Language\Printer; use GraphQL\Type\Definition\Directive; @@ -26,8 +27,8 @@ use GraphQL\Utils\Utils; use function array_filter; use function array_key_exists; +use function array_merge; use function array_values; -use function in_array; use function is_bool; use function method_exists; use function trigger_error; @@ -39,32 +40,32 @@ class Introspection const TYPE_FIELD_NAME = '__type'; const TYPE_NAME_FIELD_NAME = '__typename'; - /** @var Type[] */ + /** @var array */ private static $map = []; /** - * Options: - * - descriptions - * Whether to include descriptions in the introspection result. - * Default: true - * - * @param bool[]|bool $options + * @param array $options + * Available options: + * - descriptions + * Whether to include descriptions in the introspection result. + * Default: true + * - directiveIsRepeatable + * Whether to include `isRepeatable` flag on directives. + * Default: false * * @return string + * + * @api */ - public static function getIntrospectionQuery($options = []) + public static function getIntrospectionQuery(array $options = []) { - if (is_bool($options)) { - trigger_error( - 'Calling Introspection::getIntrospectionQuery(boolean) is deprecated. ' . - 'Please use Introspection::getIntrospectionQuery(["descriptions" => boolean]).', - E_USER_DEPRECATED - ); - $descriptions = $options; - } else { - $descriptions = ! array_key_exists('descriptions', $options) || $options['descriptions'] === true; - } - $descriptionField = $descriptions ? 'description' : ''; + $optionsWithDefaults = array_merge([ + 'descriptions' => true, + 'directiveIsRepeatable' => false, + ], $options); + + $descriptions = $optionsWithDefaults['descriptions'] ? 'description' : ''; + $directiveIsRepeatable = $optionsWithDefaults['directiveIsRepeatable'] ? 'isRepeatable' : ''; return << $options + * Available options: + * - descriptions + * Whether to include `isRepeatable` flag on directives. + * Default: true + * - directiveIsRepeatable + * Whether to include descriptions in the introspection result. + * Default: true + * + * @return array>|null + * + * @api + */ + public static function fromSchema(Schema $schema, array $options = []) : ?array + { + $optionsWithDefaults = array_merge(['directiveIsRepeatable' => true], $options); + + $result = GraphQL::executeQuery( + $schema, + self::getIntrospectionQuery($optionsWithDefaults) + ); + + return $result->data; + } + public static function _schema() { if (! isset(self::$map['__Schema'])) { @@ -200,14 +236,14 @@ public static function _schema() 'types' => [ 'description' => 'A list of all types supported by this server.', 'type' => new NonNull(new ListOfType(new NonNull(self::_type()))), - 'resolve' => static function (Schema $schema) { + 'resolve' => static function (Schema $schema) : array { return array_values($schema->getTypeMap()); }, ], 'queryType' => [ 'description' => 'The type that query operations will be rooted at.', 'type' => new NonNull(self::_type()), - 'resolve' => static function (Schema $schema) { + 'resolve' => static function (Schema $schema) : ?ObjectType { return $schema->getQueryType(); }, ], @@ -216,21 +252,21 @@ public static function _schema() 'If this server supports mutation, the type that ' . 'mutation operations will be rooted at.', 'type' => self::_type(), - 'resolve' => static function (Schema $schema) { + 'resolve' => static function (Schema $schema) : ?ObjectType { return $schema->getMutationType(); }, ], 'subscriptionType' => [ 'description' => 'If this server support subscription, the type that subscription operations will be rooted at.', 'type' => self::_type(), - 'resolve' => static function (Schema $schema) { + 'resolve' => static function (Schema $schema) : ?ObjectType { return $schema->getSubscriptionType(); }, ], 'directives' => [ 'description' => 'A list of all directives supported by this server.', 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_directive()))), - 'resolve' => static function (Schema $schema) { + 'resolve' => static function (Schema $schema) : array { return $schema->getDirectives(); }, ], @@ -264,7 +300,7 @@ public static function _type() 'resolve' => static function (Type $type) { switch (true) { case $type instanceof ListOfType: - return TypeKind::LIST_KIND; + return TypeKind::LIST; case $type instanceof NonNull: return TypeKind::NON_NULL; case $type instanceof ScalarType: @@ -276,7 +312,7 @@ public static function _type() case $type instanceof InputObjectType: return TypeKind::INPUT_OBJECT; case $type instanceof InterfaceType: - return TypeKind::INTERFACE_KIND; + return TypeKind::INTERFACE; case $type instanceof UnionType: return TypeKind::UNION; default: @@ -284,8 +320,18 @@ public static function _type() } }, ], - 'name' => ['type' => Type::string()], - 'description' => ['type' => Type::string()], + 'name' => [ + 'type' => Type::string(), + 'resolve' => static function ($obj) { + return $obj->name; + }, + ], + 'description' => [ + 'type' => Type::string(), + 'resolve' => static function ($obj) { + return $obj->description; + }, + ], 'fields' => [ 'type' => Type::listOf(Type::nonNull(self::_field())), 'args' => [ @@ -295,10 +341,10 @@ public static function _type() if ($type instanceof ObjectType || $type instanceof InterfaceType) { $fields = $type->getFields(); - if (empty($args['includeDeprecated'])) { + if (! ($args['includeDeprecated'] ?? false)) { $fields = array_filter( $fields, - static function (FieldDefinition $field) { + static function (FieldDefinition $field) : bool { return ! $field->deprecationReason; } ); @@ -312,8 +358,8 @@ static function (FieldDefinition $field) { ], 'interfaces' => [ 'type' => Type::listOf(Type::nonNull(self::_type())), - 'resolve' => static function ($type) { - if ($type instanceof ObjectType) { + 'resolve' => static function ($type) : ?array { + if ($type instanceof ObjectType || $type instanceof InterfaceType) { return $type->getInterfaces(); } @@ -322,7 +368,7 @@ static function (FieldDefinition $field) { ], 'possibleTypes' => [ 'type' => Type::listOf(Type::nonNull(self::_type())), - 'resolve' => static function ($type, $args, $context, ResolveInfo $info) { + 'resolve' => static function ($type, $args, $context, ResolveInfo $info) : ?array { if ($type instanceof InterfaceType || $type instanceof UnionType) { return $info->schema->getPossibleTypes($type); } @@ -339,10 +385,10 @@ static function (FieldDefinition $field) { if ($type instanceof EnumType) { $values = array_values($type->getValues()); - if (empty($args['includeDeprecated'])) { + if (! ($args['includeDeprecated'] ?? false)) { $values = array_filter( $values, - static function ($value) { + static function ($value) : bool { return ! $value->deprecationReason; } ); @@ -356,7 +402,7 @@ static function ($value) { ], 'inputFields' => [ 'type' => Type::listOf(Type::nonNull(self::_inputValue())), - 'resolve' => static function ($type) { + 'resolve' => static function ($type) : ?array { if ($type instanceof InputObjectType) { return array_values($type->getFields()); } @@ -366,7 +412,7 @@ static function ($value) { ], 'ofType' => [ 'type' => self::_type(), - 'resolve' => static function ($type) { + 'resolve' => static function ($type) : ?Type { if ($type instanceof WrappingType) { return $type->getWrappedType(); } @@ -399,8 +445,8 @@ public static function _typeKind() 'description' => 'Indicates this type is an object. `fields` and `interfaces` are valid fields.', ], 'INTERFACE' => [ - 'value' => TypeKind::INTERFACE_KIND, - 'description' => 'Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.', + 'value' => TypeKind::INTERFACE, + 'description' => 'Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.', ], 'UNION' => [ 'value' => TypeKind::UNION, @@ -415,7 +461,7 @@ public static function _typeKind() 'description' => 'Indicates this type is an input object. `inputFields` is a valid field.', ], 'LIST' => [ - 'value' => TypeKind::LIST_KIND, + 'value' => TypeKind::LIST, 'description' => 'Indicates this type is a list. `ofType` is a valid field.', ], 'NON_NULL' => [ @@ -440,28 +486,41 @@ public static function _field() 'which has a name, potentially a list of arguments, and a return type.', 'fields' => static function () { return [ - 'name' => ['type' => Type::nonNull(Type::string())], - 'description' => ['type' => Type::string()], + 'name' => [ + 'type' => Type::nonNull(Type::string()), + 'resolve' => static function (FieldDefinition $field) : string { + return $field->name; + }, + ], + 'description' => [ + 'type' => Type::string(), + 'resolve' => static function (FieldDefinition $field) : ?string { + return $field->description; + }, + ], 'args' => [ 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))), - 'resolve' => static function (FieldDefinition $field) { - return empty($field->args) ? [] : $field->args; + 'resolve' => static function (FieldDefinition $field) : array { + return $field->args ?? []; }, ], 'type' => [ 'type' => Type::nonNull(self::_type()), - 'resolve' => static function (FieldDefinition $field) { + 'resolve' => static function (FieldDefinition $field) : Type { return $field->getType(); }, ], 'isDeprecated' => [ 'type' => Type::nonNull(Type::boolean()), - 'resolve' => static function (FieldDefinition $field) { + 'resolve' => static function (FieldDefinition $field) : bool { return (bool) $field->deprecationReason; }, ], 'deprecationReason' => [ - 'type' => Type::string(), + 'type' => Type::string(), + 'resolve' => static function (FieldDefinition $field) : ?string { + return $field->deprecationReason; + }, ], ]; }, @@ -483,20 +542,40 @@ public static function _inputValue() 'and optionally a default value.', 'fields' => static function () { return [ - 'name' => ['type' => Type::nonNull(Type::string())], - 'description' => ['type' => Type::string()], + 'name' => [ + 'type' => Type::nonNull(Type::string()), + 'resolve' => static function ($inputValue) : string { + /** @var FieldArgument|InputObjectField $inputValue */ + $inputValue = $inputValue; + + return $inputValue->name; + }, + ], + 'description' => [ + 'type' => Type::string(), + 'resolve' => static function ($inputValue) : ?string { + /** @var FieldArgument|InputObjectField $inputValue */ + $inputValue = $inputValue; + + return $inputValue->description; + }, + ], 'type' => [ 'type' => Type::nonNull(self::_type()), 'resolve' => static function ($value) { - return method_exists($value, 'getType') ? $value->getType() : $value->type; + return method_exists($value, 'getType') + ? $value->getType() + : $value->type; }, ], 'defaultValue' => [ 'type' => Type::string(), 'description' => 'A GraphQL-formatted string representing the default value for this input value.', - 'resolve' => static function ($inputValue) { + 'resolve' => static function ($inputValue) : ?string { /** @var FieldArgument|InputObjectField $inputValue */ + $inputValue = $inputValue; + return ! $inputValue->defaultValueExists() ? null : Printer::doPrint(AST::astFromValue( @@ -524,16 +603,29 @@ public static function _enumValue() 'a placeholder for a string or numeric value. However an Enum value is ' . 'returned in a JSON response as a string.', 'fields' => [ - 'name' => ['type' => Type::nonNull(Type::string())], - 'description' => ['type' => Type::string()], + 'name' => [ + 'type' => Type::nonNull(Type::string()), + 'resolve' => static function ($enumValue) { + return $enumValue->name; + }, + ], + 'description' => [ + 'type' => Type::string(), + 'resolve' => static function ($enumValue) { + return $enumValue->description; + }, + ], 'isDeprecated' => [ 'type' => Type::nonNull(Type::boolean()), - 'resolve' => static function ($enumValue) { + 'resolve' => static function ($enumValue) : bool { return (bool) $enumValue->deprecationReason; }, ], 'deprecationReason' => [ 'type' => Type::string(), + 'resolve' => static function ($enumValue) { + return $enumValue->deprecationReason; + }, ], ], ]); @@ -555,45 +647,36 @@ public static function _directive() 'conditionally including or skipping a field. Directives provide this by ' . 'describing additional information to the executor.', 'fields' => [ - 'name' => ['type' => Type::nonNull(Type::string())], - 'description' => ['type' => Type::string()], - 'locations' => [ - 'type' => Type::nonNull(Type::listOf(Type::nonNull( - self::_directiveLocation() - ))), + 'name' => [ + 'type' => Type::nonNull(Type::string()), + 'resolve' => static function ($obj) { + return $obj->name; + }, + ], + 'description' => [ + 'type' => Type::string(), + 'resolve' => static function ($obj) { + return $obj->description; + }, ], 'args' => [ 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))), 'resolve' => static function (Directive $directive) { - return $directive->args ?: []; + return $directive->args ?? []; }, ], - - // NOTE: the following three fields are deprecated and are no longer part - // of the GraphQL specification. - 'onOperation' => [ - 'deprecationReason' => 'Use `locations`.', - 'type' => Type::nonNull(Type::boolean()), - 'resolve' => static function ($d) { - return in_array(DirectiveLocation::QUERY, $d->locations, true) || - in_array(DirectiveLocation::MUTATION, $d->locations, true) || - in_array(DirectiveLocation::SUBSCRIPTION, $d->locations, true); - }, - ], - 'onFragment' => [ - 'deprecationReason' => 'Use `locations`.', - 'type' => Type::nonNull(Type::boolean()), - 'resolve' => static function ($d) { - return in_array(DirectiveLocation::FRAGMENT_SPREAD, $d->locations, true) || - in_array(DirectiveLocation::INLINE_FRAGMENT, $d->locations, true) || - in_array(DirectiveLocation::FRAGMENT_DEFINITION, $d->locations, true); + 'isRepeatable' => [ + 'type' => Type::nonNull(Type::boolean()), + 'resolve' => static function (Directive $directive) : bool { + return $directive->isRepeatable; }, ], - 'onField' => [ - 'deprecationReason' => 'Use `locations`.', - 'type' => Type::nonNull(Type::boolean()), - 'resolve' => static function ($d) { - return in_array(DirectiveLocation::FIELD, $d->locations, true); + 'locations' => [ + 'type' => Type::nonNull(Type::listOf(Type::nonNull( + self::_directiveLocation() + ))), + 'resolve' => static function ($obj) { + return $obj->locations; }, ], ], @@ -641,6 +724,10 @@ public static function _directiveLocation() 'value' => DirectiveLocation::INLINE_FRAGMENT, 'description' => 'Location adjacent to an inline fragment.', ], + 'VARIABLE_DEFINITION' => [ + 'value' => DirectiveLocation::VARIABLE_DEFINITION, + 'description' => 'Location adjacent to a variable definition.', + ], 'SCHEMA' => [ 'value' => DirectiveLocation::SCHEMA, 'description' => 'Location adjacent to a schema definition.', @@ -693,7 +780,7 @@ public static function _directiveLocation() return self::$map['__DirectiveLocation']; } - public static function schemaMetaFieldDef() + public static function schemaMetaFieldDef() : FieldDefinition { if (! isset(self::$map[self::SCHEMA_FIELD_NAME])) { self::$map[self::SCHEMA_FIELD_NAME] = FieldDefinition::create([ @@ -706,7 +793,7 @@ public static function schemaMetaFieldDef() $args, $context, ResolveInfo $info - ) { + ) : Schema { return $info->schema; }, ]); @@ -715,7 +802,7 @@ public static function schemaMetaFieldDef() return self::$map[self::SCHEMA_FIELD_NAME]; } - public static function typeMetaFieldDef() + public static function typeMetaFieldDef() : FieldDefinition { if (! isset(self::$map[self::TYPE_FIELD_NAME])) { self::$map[self::TYPE_FIELD_NAME] = FieldDefinition::create([ @@ -725,7 +812,7 @@ public static function typeMetaFieldDef() 'args' => [ ['name' => 'name', 'type' => Type::nonNull(Type::string())], ], - 'resolve' => static function ($source, $args, $context, ResolveInfo $info) { + 'resolve' => static function ($source, $args, $context, ResolveInfo $info) : Type { return $info->schema->getType($args['name']); }, ]); @@ -734,7 +821,7 @@ public static function typeMetaFieldDef() return self::$map[self::TYPE_FIELD_NAME]; } - public static function typeNameMetaFieldDef() + public static function typeNameMetaFieldDef() : FieldDefinition { if (! isset(self::$map[self::TYPE_NAME_FIELD_NAME])) { self::$map[self::TYPE_NAME_FIELD_NAME] = FieldDefinition::create([ @@ -747,7 +834,7 @@ public static function typeNameMetaFieldDef() $args, $context, ResolveInfo $info - ) { + ) : string { return $info->parentType->name; }, ]); diff --git a/src/Type/Schema.php b/src/Type/Schema.php index 7e480e824..f6a93309a 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -12,13 +12,16 @@ use GraphQL\Language\AST\SchemaTypeExtensionNode; use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\Directive; +use GraphQL\Type\Definition\ImplementingType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; +use GraphQL\Utils\InterfaceImplementations; use GraphQL\Utils\TypeInfo; use GraphQL\Utils\Utils; use Traversable; +use function array_map; use function array_values; use function implode; use function is_array; @@ -57,8 +60,19 @@ class Schema */ private $resolvedTypes = []; - /** @var Type[][]|null */ - private $possibleTypeMap; + /** + * Lazily initialized. + * + * @var array> + */ + private $subTypeMap; + + /** + * Lazily initialised. + * + * @var array + */ + private $implementationsMap; /** * True when $resolvedTypes contain all possible schema types @@ -67,11 +81,11 @@ class Schema */ private $fullyLoaded = false; - /** @var InvariantViolation[]|null */ + /** @var Error[] */ private $validationErrors; /** @var SchemaTypeExtensionNode[] */ - public $extensionASTNodes; + public $extensionASTNodes = []; /** * @param mixed[]|SchemaConfig $config @@ -112,7 +126,7 @@ public function __construct($config) '"types" must be array or callable if provided but got: ' . Utils::getVariableType($config->types) ); Utils::invariant( - ! $config->directives || is_array($config->directives), + $config->directives === null || is_array($config->directives), '"directives" must be Array if provided but got: ' . Utils::getVariableType($config->directives) ); } @@ -120,13 +134,13 @@ public function __construct($config) $this->config = $config; $this->extensionASTNodes = $config->extensionASTNodes; - if ($config->query) { + if ($config->query !== null) { $this->resolvedTypes[$config->query->name] = $config->query; } - if ($config->mutation) { + if ($config->mutation !== null) { $this->resolvedTypes[$config->mutation->name] = $config->mutation; } - if ($config->subscription) { + if ($config->subscription !== null) { $this->resolvedTypes[$config->subscription->name] = $config->subscription; } if (is_array($this->config->types)) { @@ -158,7 +172,7 @@ public function __construct($config) */ private function resolveAdditionalTypes() { - $types = $this->config->types ?: []; + $types = $this->config->types ?? []; if (is_callable($types)) { $types = $types(); @@ -172,6 +186,7 @@ private function resolveAdditionalTypes() } foreach ($types as $index => $type) { + $type = self::resolveType($type); if (! $type instanceof Type) { throw new InvariantViolation(sprintf( 'Each entry of schema types must be instance of GraphQL\Type\Definition\Type but entry at %s is %s', @@ -189,11 +204,11 @@ private function resolveAdditionalTypes() * * This operation requires full schema scan. Do not use in production environment. * - * @return Type[] + * @return array * * @api */ - public function getTypeMap() + public function getTypeMap() : array { if (! $this->fullyLoaded) { $this->resolvedTypes = $this->collectAllTypes(); @@ -238,7 +253,26 @@ private function collectAllTypes() */ public function getDirectives() { - return $this->config->directives ?: GraphQL::getStandardDirectives(); + return $this->config->directives ?? GraphQL::getStandardDirectives(); + } + + /** + * @param string $operation + * + * @return ObjectType|null + */ + public function getOperationType($operation) + { + switch ($operation) { + case 'query': + return $this->getQueryType(); + case 'mutation': + return $this->getMutationType(); + case 'subscription': + return $this->getSubscriptionType(); + default: + return null; + } } /** @@ -248,7 +282,7 @@ public function getDirectives() * * @api */ - public function getQueryType() + public function getQueryType() : ?Type { return $this->config->query; } @@ -260,7 +294,7 @@ public function getQueryType() * * @api */ - public function getMutationType() + public function getMutationType() : ?Type { return $this->config->mutation; } @@ -272,7 +306,7 @@ public function getMutationType() * * @api */ - public function getSubscriptionType() + public function getSubscriptionType() : ?Type { return $this->config->subscription; } @@ -288,61 +322,56 @@ public function getConfig() } /** - * Returns type by it's name - * - * @param string $name - * - * @return Type|null + * Returns type by its name * * @api */ - public function getType($name) + public function getType(string $name) : ?Type { if (! isset($this->resolvedTypes[$name])) { $type = $this->loadType($name); + if (! $type) { return null; } - $this->resolvedTypes[$name] = $type; + $this->resolvedTypes[$name] = self::resolveType($type); } return $this->resolvedTypes[$name]; } - /** - * @param string $name - * - * @return bool - */ - public function hasType($name) + public function hasType(string $name) : bool { return $this->getType($name) !== null; } - /** - * @param string $typeName - * - * @return Type - */ - private function loadType($typeName) + private function loadType(string $typeName) : ?Type { $typeLoader = $this->config->typeLoader; - if (! $typeLoader) { + if (! isset($typeLoader)) { return $this->defaultTypeLoader($typeName); } $type = $typeLoader($typeName); if (! $type instanceof Type) { - throw new InvariantViolation( - sprintf( - 'Type loader is expected to return valid type "%s", but it returned %s', - $typeName, - Utils::printSafe($type) - ) - ); + // Unless you know what you're doing, kindly resist the temptation to refactor or simplify this block. The + // twisty logic here is tuned for performance, and meant to prioritize the "happy path" (the result returned + // from the type loader is already a Type), and only checks for callable if that fails. If the result is + // neither a Type nor a callable, then we throw an exception. + + if (is_callable($type)) { + $type = $type(); + + if (! $type instanceof Type) { + $this->throwNotAType($type, $typeName); + } + } else { + $this->throwNotAType($type, $typeName); + } } + if ($type->name !== $typeName) { throw new InvariantViolation( sprintf('Type loader is expected to return type "%s", but it returned "%s"', $typeName, $type->name) @@ -352,91 +381,159 @@ private function loadType($typeName) return $type; } - /** - * @param string $typeName - * - * @return Type - */ - private function defaultTypeLoader($typeName) + protected function throwNotAType($type, string $typeName) { - // Default type loader simply fallbacks to collecting all types + throw new InvariantViolation( + sprintf( + 'Type loader is expected to return a callable or valid type "%s", but it returned %s', + $typeName, + Utils::printSafe($type) + ) + ); + } + + private function defaultTypeLoader(string $typeName) : ?Type + { + // Default type loader simply falls back to collecting all types $typeMap = $this->getTypeMap(); return $typeMap[$typeName] ?? null; } + /** + * @param Type|callable():Type $type + */ + public static function resolveType($type) : Type + { + if ($type instanceof Type) { + return $type; + } + + return $type(); + } + /** * Returns all possible concrete types for given abstract type * (implementations for interfaces and members of union type for unions) * * This operation requires full schema scan. Do not use in production environment. * - * @return ObjectType[] + * @param InterfaceType|UnionType $abstractType + * + * @return array * * @api */ - public function getPossibleTypes(AbstractType $abstractType) + public function getPossibleTypes(Type $abstractType) : array { - $possibleTypeMap = $this->getPossibleTypeMap(); + return $abstractType instanceof UnionType + ? $abstractType->getTypes() + : $this->getImplementations($abstractType)->objects(); + } - return isset($possibleTypeMap[$abstractType->name]) ? array_values($possibleTypeMap[$abstractType->name]) : []; + /** + * Returns all types that implement a given interface type. + * + * This operations requires full schema scan. Do not use in production environment. + * + * @api + */ + public function getImplementations(InterfaceType $abstractType) : InterfaceImplementations + { + return $this->collectImplementations()[$abstractType->name]; } /** - * @return Type[][] + * @return array */ - private function getPossibleTypeMap() + private function collectImplementations() : array { - if ($this->possibleTypeMap === null) { - $this->possibleTypeMap = []; + if (! isset($this->implementationsMap)) { + /** @var array> $foundImplementations */ + $foundImplementations = []; foreach ($this->getTypeMap() as $type) { - if ($type instanceof ObjectType) { - foreach ($type->getInterfaces() as $interface) { - if (! ($interface instanceof InterfaceType)) { - continue; - } + if ($type instanceof InterfaceType) { + if (! isset($foundImplementations[$type->name])) { + $foundImplementations[$type->name] = ['objects' => [], 'interfaces' => []]; + } - $this->possibleTypeMap[$interface->name][$type->name] = $type; + foreach ($type->getInterfaces() as $iface) { + if (! isset($foundImplementations[$iface->name])) { + $foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []]; + } + $foundImplementations[$iface->name]['interfaces'][] = $type; } - } elseif ($type instanceof UnionType) { - foreach ($type->getTypes() as $innerType) { - $this->possibleTypeMap[$type->name][$innerType->name] = $innerType; + } elseif ($type instanceof ObjectType) { + foreach ($type->getInterfaces() as $iface) { + if (! isset($foundImplementations[$iface->name])) { + $foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []]; + } + $foundImplementations[$iface->name]['objects'][] = $type; } } } + $this->implementationsMap = array_map( + static function (array $implementations) : InterfaceImplementations { + return new InterfaceImplementations($implementations['objects'], $implementations['interfaces']); + }, + $foundImplementations + ); } - return $this->possibleTypeMap; + return $this->implementationsMap; } /** + * @deprecated as of 14.4.0 use isSubType instead, will be removed in 15.0.0. + * * Returns true if object type is concrete type of given abstract type * (implementation for interfaces and members of union type for unions) * - * @return bool + * @api + * @codeCoverageIgnore + */ + public function isPossibleType(AbstractType $abstractType, ObjectType $possibleType) : bool + { + return $this->isSubType($abstractType, $possibleType); + } + + /** + * Returns true if the given type is a sub type of the given abstract type. + * + * @param UnionType|InterfaceType $abstractType + * @param ObjectType|InterfaceType $maybeSubType * * @api */ - public function isPossibleType(AbstractType $abstractType, ObjectType $possibleType) + public function isSubType(AbstractType $abstractType, ImplementingType $maybeSubType) : bool { - if ($abstractType instanceof InterfaceType) { - return $possibleType->implementsInterface($abstractType); + if (! isset($this->subTypeMap[$abstractType->name])) { + $this->subTypeMap[$abstractType->name] = []; + + if ($abstractType instanceof UnionType) { + foreach ($abstractType->getTypes() as $type) { + $this->subTypeMap[$abstractType->name][$type->name] = true; + } + } else { + $implementations = $this->getImplementations($abstractType); + foreach ($implementations->objects() as $type) { + $this->subTypeMap[$abstractType->name][$type->name] = true; + } + foreach ($implementations->interfaces() as $type) { + $this->subTypeMap[$abstractType->name][$type->name] = true; + } + } } - /** @var UnionType $abstractType */ - return $abstractType->isPossibleType($possibleType); + return isset($this->subTypeMap[$abstractType->name][$maybeSubType->name]); } /** * Returns instance of directive by name * - * @param string $name - * - * @return Directive - * * @api */ - public function getDirective($name) + public function getDirective(string $name) : ?Directive { foreach ($this->getDirectives() as $directive) { if ($directive->name === $name) { @@ -447,10 +544,7 @@ public function getDirective($name) return null; } - /** - * @return SchemaDefinitionNode - */ - public function getAstNode() + public function getAstNode() : ?SchemaDefinitionNode { return $this->config->getAstNode(); } diff --git a/src/Type/SchemaConfig.php b/src/Type/SchemaConfig.php index 9f336262b..f3511d782 100644 --- a/src/Type/SchemaConfig.php +++ b/src/Type/SchemaConfig.php @@ -10,6 +10,7 @@ use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Utils\Utils; +use function count; use function is_callable; /** @@ -27,32 +28,32 @@ */ class SchemaConfig { - /** @var ObjectType */ + /** @var ObjectType|null */ public $query; - /** @var ObjectType */ + /** @var ObjectType|null */ public $mutation; - /** @var ObjectType */ + /** @var ObjectType|null */ public $subscription; /** @var Type[]|callable */ - public $types; + public $types = []; - /** @var Directive[] */ + /** @var Directive[]|null */ public $directives; - /** @var callable */ + /** @var callable|null */ public $typeLoader; - /** @var SchemaDefinitionNode */ + /** @var SchemaDefinitionNode|null */ public $astNode; /** @var bool */ - public $assumeValid; + public $assumeValid = false; /** @var SchemaTypeExtensionNode[] */ - public $extensionASTNodes; + public $extensionASTNodes = []; /** * Converts an array of options to instance of SchemaConfig @@ -68,7 +69,7 @@ public static function create(array $options = []) { $config = new static(); - if (! empty($options)) { + if (count($options) > 0) { if (isset($options['query'])) { $config->setQuery($options['query']); } @@ -115,7 +116,7 @@ public static function create(array $options = []) } /** - * @return SchemaDefinitionNode + * @return SchemaDefinitionNode|null */ public function getAstNode() { @@ -133,7 +134,7 @@ public function setAstNode(SchemaDefinitionNode $astNode) } /** - * @return ObjectType + * @return ObjectType|null * * @api */ @@ -143,7 +144,7 @@ public function getQuery() } /** - * @param ObjectType $query + * @param ObjectType|null $query * * @return SchemaConfig * @@ -157,7 +158,7 @@ public function setQuery($query) } /** - * @return ObjectType + * @return ObjectType|null * * @api */ @@ -167,7 +168,7 @@ public function getMutation() } /** - * @param ObjectType $mutation + * @param ObjectType|null $mutation * * @return SchemaConfig * @@ -181,7 +182,7 @@ public function setMutation($mutation) } /** - * @return ObjectType + * @return ObjectType|null * * @api */ @@ -191,7 +192,7 @@ public function getSubscription() } /** - * @param ObjectType $subscription + * @param ObjectType|null $subscription * * @return SchemaConfig * @@ -205,13 +206,13 @@ public function setSubscription($subscription) } /** - * @return Type[] + * @return Type[]|callable * * @api */ public function getTypes() { - return $this->types ?: []; + return $this->types; } /** @@ -229,13 +230,13 @@ public function setTypes($types) } /** - * @return Directive[] + * @return Directive[]|null * * @api */ public function getDirectives() { - return $this->directives ?: []; + return $this->directives; } /** @@ -253,7 +254,7 @@ public function setDirectives(array $directives) } /** - * @return callable + * @return callable|null * * @api */ diff --git a/src/Type/SchemaValidationContext.php b/src/Type/SchemaValidationContext.php index 63f54dd46..7c5704393 100644 --- a/src/Type/SchemaValidationContext.php +++ b/src/Type/SchemaValidationContext.php @@ -5,39 +5,48 @@ namespace GraphQL\Type; use GraphQL\Error\Error; +use GraphQL\Language\AST\DirectiveDefinitionNode; +use GraphQL\Language\AST\DirectiveNode; use GraphQL\Language\AST\EnumValueDefinitionNode; use GraphQL\Language\AST\FieldDefinitionNode; use GraphQL\Language\AST\InputValueDefinitionNode; use GraphQL\Language\AST\InterfaceTypeDefinitionNode; use GraphQL\Language\AST\InterfaceTypeExtensionNode; +use GraphQL\Language\AST\ListTypeNode; use GraphQL\Language\AST\NamedTypeNode; use GraphQL\Language\AST\Node; +use GraphQL\Language\AST\NodeList; +use GraphQL\Language\AST\NonNullTypeNode; use GraphQL\Language\AST\ObjectTypeDefinitionNode; use GraphQL\Language\AST\ObjectTypeExtensionNode; use GraphQL\Language\AST\SchemaDefinitionNode; use GraphQL\Language\AST\TypeDefinitionNode; use GraphQL\Language\AST\TypeNode; +use GraphQL\Language\DirectiveLocation; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumValueDefinition; use GraphQL\Type\Definition\FieldDefinition; +use GraphQL\Type\Definition\ImplementingType; use GraphQL\Type\Definition\InputObjectField; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\NamedType; use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; +use GraphQL\Type\Validation\InputObjectCircularRefs; use GraphQL\Utils\TypeComparators; use GraphQL\Utils\Utils; use function array_filter; use function array_key_exists; use function array_merge; use function count; +use function in_array; use function is_array; use function is_object; -use function iterator_to_array; use function sprintf; class SchemaValidationContext @@ -48,9 +57,13 @@ class SchemaValidationContext /** @var Schema */ private $schema; + /** @var InputObjectCircularRefs */ + private $inputObjectCircularRefs; + public function __construct(Schema $schema) { - $this->schema = $schema; + $this->schema = $schema; + $this->inputObjectCircularRefs = new InputObjectCircularRefs($this); } /** @@ -61,7 +74,7 @@ public function getErrors() return $this->errors; } - public function validateRootTypes() + public function validateRootTypes() : void { $queryType = $this->schema->getQueryType(); if (! $queryType) { @@ -85,7 +98,7 @@ public function validateRootTypes() } $subscriptionType = $this->schema->getSubscriptionType(); - if (! $subscriptionType || $subscriptionType instanceof ObjectType) { + if ($subscriptionType === null || $subscriptionType instanceof ObjectType) { return; } @@ -99,7 +112,7 @@ public function validateRootTypes() * @param string $message * @param Node[]|Node|TypeNode|TypeDefinitionNode|null $nodes */ - private function reportError($message, $nodes = null) + public function reportError($message, $nodes = null) { $nodes = array_filter($nodes && is_array($nodes) ? $nodes : [$nodes]); $this->addError(new Error($message, $nodes)); @@ -117,7 +130,7 @@ private function addError($error) * @param Type $type * @param string $operation * - * @return TypeNode|TypeDefinitionNode + * @return NamedTypeNode|ListTypeNode|NonNullTypeNode|TypeDefinitionNode */ private function getOperationTypeNode($type, $operation) { @@ -140,16 +153,36 @@ private function getOperationTypeNode($type, $operation) public function validateDirectives() { + $this->validateDirectiveDefinitions(); + + // Validate directives that are used on the schema + $this->validateDirectivesAtLocation( + $this->getDirectives($this->schema), + DirectiveLocation::SCHEMA + ); + } + + public function validateDirectiveDefinitions() + { + $directiveDefinitions = []; + $directives = $this->schema->getDirectives(); foreach ($directives as $directive) { // Ensure all directives are in fact GraphQL directives. if (! $directive instanceof Directive) { + $nodes = is_object($directive) + ? $directive->astNode + : null; + $this->reportError( 'Expected directive but got: ' . Utils::printSafe($directive) . '.', - is_object($directive) ? $directive->astNode : null + $nodes ); continue; } + $existingDefinitions = $directiveDefinitions[$directive->name] ?? []; + $existingDefinitions[] = $directive; + $directiveDefinitions[$directive->name] = $existingDefinitions; // Ensure they are named correctly. $this->validateName($directive); @@ -189,6 +222,22 @@ public function validateDirectives() ); } } + foreach ($directiveDefinitions as $directiveName => $directiveList) { + if (count($directiveList) <= 1) { + continue; + } + + $nodes = Utils::map( + $directiveList, + static function (Directive $directive) : ?DirectiveDefinitionNode { + return $directive->astNode; + } + ); + $this->reportError( + sprintf('Directive @%s defined multiple times.', $directiveName), + array_filter($nodes) + ); + } } /** @@ -212,34 +261,34 @@ private function validateName($node) */ private function getAllDirectiveArgNodes(Directive $directive, $argName) { - $argNodes = []; - $directiveNode = $directive->astNode; - if ($directiveNode && $directiveNode->arguments) { - foreach ($directiveNode->arguments as $node) { - if ($node->name->value !== $argName) { - continue; - } - - $argNodes[] = $node; + $subNodes = $this->getAllSubNodes( + $directive, + static function ($directiveNode) { + return $directiveNode->arguments; } - } + ); - return $argNodes; + return Utils::filter( + $subNodes, + static function ($argNode) use ($argName) : bool { + return $argNode->name->value === $argName; + } + ); } /** * @param string $argName * - * @return TypeNode|null + * @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null */ - private function getDirectiveArgTypeNode(Directive $directive, $argName) + private function getDirectiveArgTypeNode(Directive $directive, $argName) : ?TypeNode { $argNode = $this->getAllDirectiveArgNodes($directive, $argName)[0]; return $argNode ? $argNode->type : null; } - public function validateTypes() + public function validateTypes() : void { $typeMap = $this->schema->getTypeMap(); foreach ($typeMap as $typeName => $type) { @@ -247,7 +296,7 @@ public function validateTypes() if (! $type instanceof NamedType) { $this->reportError( 'Expected GraphQL named type but got: ' . Utils::printSafe($type) . '.', - is_object($type) ? $type->astNode : null + $type instanceof Type ? $type->astNode : null ); continue; } @@ -259,23 +308,113 @@ public function validateTypes() $this->validateFields($type); // Ensure objects implement the interfaces they claim to. - $this->validateObjectInterfaces($type); + $this->validateInterfaces($type); + + // Ensure directives are valid + $this->validateDirectivesAtLocation( + $this->getDirectives($type), + DirectiveLocation::OBJECT + ); } elseif ($type instanceof InterfaceType) { // Ensure fields are valid. $this->validateFields($type); - // Ensure Interfaces include at least 1 Object type. + // Ensure interfaces implement the interfaces they claim to. $this->validateInterfaces($type); + + // Ensure directives are valid + $this->validateDirectivesAtLocation( + $this->getDirectives($type), + DirectiveLocation::IFACE + ); } elseif ($type instanceof UnionType) { // Ensure Unions include valid member types. $this->validateUnionMembers($type); + + // Ensure directives are valid + $this->validateDirectivesAtLocation( + $this->getDirectives($type), + DirectiveLocation::UNION + ); } elseif ($type instanceof EnumType) { // Ensure Enums have valid values. $this->validateEnumValues($type); + + // Ensure directives are valid + $this->validateDirectivesAtLocation( + $this->getDirectives($type), + DirectiveLocation::ENUM + ); } elseif ($type instanceof InputObjectType) { // Ensure Input Object fields are valid. $this->validateInputFields($type); + + // Ensure directives are valid + $this->validateDirectivesAtLocation( + $this->getDirectives($type), + DirectiveLocation::INPUT_OBJECT + ); + + // Ensure Input Objects do not contain non-nullable circular references + $this->inputObjectCircularRefs->validate($type); + } elseif ($type instanceof ScalarType) { + // Ensure directives are valid + $this->validateDirectivesAtLocation( + $this->getDirectives($type), + DirectiveLocation::SCALAR + ); + } + } + } + + /** + * @param NodeList $directives + */ + private function validateDirectivesAtLocation($directives, string $location) + { + $directivesNamed = []; + $schema = $this->schema; + foreach ($directives as $directive) { + $directiveName = $directive->name->value; + + // Ensure directive used is also defined + $schemaDirective = $schema->getDirective($directiveName); + if ($schemaDirective === null) { + $this->reportError( + sprintf('No directive @%s defined.', $directiveName), + $directive + ); + continue; + } + $includes = Utils::some( + $schemaDirective->locations, + static function ($schemaLocation) use ($location) : bool { + return $schemaLocation === $location; + } + ); + if (! $includes) { + $errorNodes = $schemaDirective->astNode + ? [$directive, $schemaDirective->astNode] + : [$directive]; + $this->reportError( + sprintf('Directive @%s not allowed at %s location.', $directiveName, $location), + $errorNodes + ); + } + + $existingNodes = $directivesNamed[$directiveName] ?? []; + $existingNodes[] = $directive; + $directivesNamed[$directiveName] = $existingNodes; + } + foreach ($directivesNamed as $directiveName => $directiveList) { + if (count($directiveList) <= 1) { + continue; } + + $this->reportError( + sprintf('Directive @%s used twice at the same location.', $directiveName), + $directiveList + ); } } @@ -290,7 +429,7 @@ private function validateFields($type) if (! $fieldMap) { $this->reportError( sprintf('Type %s must define one or more fields.', $type->name), - $this->getAllObjectOrInterfaceNodes($type) + $this->getAllNodes($type) ); } @@ -343,72 +482,110 @@ private function validateFields($type) $argNames[$argName] = true; // Ensure the type is an input type - if (Type::isInputType($arg->getType())) { + if (! Type::isInputType($arg->getType())) { + $this->reportError( + sprintf( + 'The type of %s.%s(%s:) must be Input Type but got: %s.', + $type->name, + $fieldName, + $argName, + Utils::printSafe($arg->getType()) + ), + $this->getFieldArgTypeNode($type, $fieldName, $argName) + ); + } + + // Ensure argument definition directives are valid + if (! isset($arg->astNode, $arg->astNode->directives)) { continue; } - $this->reportError( - sprintf( - 'The type of %s.%s(%s:) must be Input Type but got: %s.', - $type->name, - $fieldName, - $argName, - Utils::printSafe($arg->getType()) - ), - $this->getFieldArgTypeNode($type, $fieldName, $argName) + $this->validateDirectivesAtLocation( + $arg->astNode->directives, + DirectiveLocation::ARGUMENT_DEFINITION ); } + + // Ensure any directives are valid + if (! isset($field->astNode, $field->astNode->directives)) { + continue; + } + + $this->validateDirectivesAtLocation( + $field->astNode->directives, + DirectiveLocation::FIELD_DEFINITION + ); } } /** - * @param ObjectType|InterfaceType $type + * @param Schema|ObjectType|InterfaceType|UnionType|EnumType|InputObjectType|Directive $obj * * @return ObjectTypeDefinitionNode[]|ObjectTypeExtensionNode[]|InterfaceTypeDefinitionNode[]|InterfaceTypeExtensionNode[] */ - private function getAllObjectOrInterfaceNodes($type) + private function getAllNodes($obj) { - return $type->astNode - ? ($type->extensionASTNodes - ? array_merge([$type->astNode], $type->extensionASTNodes) - : [$type->astNode]) - : ($type->extensionASTNodes ?: []); + if ($obj instanceof Schema) { + $astNode = $obj->getAstNode(); + $extensionNodes = $obj->extensionASTNodes; + } else { + $astNode = $obj->astNode; + $extensionNodes = $obj->extensionASTNodes; + } + + return $astNode + ? ($extensionNodes + ? array_merge([$astNode], $extensionNodes) + : [$astNode]) + : ($extensionNodes ?? []); } /** - * @param ObjectType|InterfaceType $type - * @param string $fieldName - * - * @return FieldDefinitionNode[] + * @param Schema|ObjectType|InterfaceType|UnionType|EnumType|Directive $obj */ - private function getAllFieldNodes($type, $fieldName) + private function getAllSubNodes($obj, callable $getter) : NodeList { - $fieldNodes = []; - $astNodes = $this->getAllObjectOrInterfaceNodes($type); - foreach ($astNodes as $astNode) { - if (! $astNode || ! $astNode->fields) { + $result = new NodeList([]); + foreach ($this->getAllNodes($obj) as $astNode) { + if (! $astNode) { continue; } - foreach ($astNode->fields as $node) { - if ($node->name->value !== $fieldName) { - continue; - } - - $fieldNodes[] = $node; + $subNodes = $getter($astNode); + if (! $subNodes) { + continue; } + + $result = $result->merge($subNodes); } - return $fieldNodes; + return $result; + } + + /** + * @param ObjectType|InterfaceType $type + * @param string $fieldName + * + * @return FieldDefinitionNode[] + */ + private function getAllFieldNodes($type, $fieldName) + { + $subNodes = $this->getAllSubNodes($type, static function ($typeNode) { + return $typeNode->fields; + }); + + return Utils::filter($subNodes, static function ($fieldNode) use ($fieldName) : bool { + return $fieldNode->name->value === $fieldName; + }); } /** * @param ObjectType|InterfaceType $type * @param string $fieldName * - * @return TypeNode|null + * @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null */ - private function getFieldTypeNode($type, $fieldName) + private function getFieldTypeNode($type, $fieldName) : ?TypeNode { $fieldNode = $this->getFieldNode($type, $fieldName); @@ -457,9 +634,9 @@ private function getAllFieldArgNodes($type, $fieldName, $argName) * @param string $fieldName * @param string $argName * - * @return TypeNode|null + * @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null */ - private function getFieldArgTypeNode($type, $fieldName, $argName) + private function getFieldArgTypeNode($type, $fieldName, $argName) : ?TypeNode { $fieldArgNode = $this->getFieldArgNode($type, $fieldName, $argName); @@ -480,122 +657,125 @@ private function getFieldArgNode($type, $fieldName, $argName) return $nodes[0] ?? null; } - private function validateObjectInterfaces(ObjectType $object) + /** + * @param ObjectType|InterfaceType $type + */ + private function validateInterfaces(ImplementingType $type) : void { - $implementedTypeNames = []; - foreach ($object->getInterfaces() as $iface) { + $ifaceTypeNames = []; + foreach ($type->getInterfaces() as $iface) { if (! $iface instanceof InterfaceType) { $this->reportError( sprintf( 'Type %s must only implement Interface types, it cannot implement %s.', - $object->name, + $type->name, Utils::printSafe($iface) ), - $this->getImplementsInterfaceNode($object, $iface) + $this->getImplementsInterfaceNode($type, $iface) ); continue; } - if (isset($implementedTypeNames[$iface->name])) { + + if ($type === $iface) { $this->reportError( - sprintf('Type %s can only implement %s once.', $object->name, $iface->name), - $this->getAllImplementsInterfaceNodes($object, $iface) + sprintf( + 'Type %s cannot implement itself because it would create a circular reference.', + $type->name + ), + $this->getImplementsInterfaceNode($type, $iface) ); continue; } - $implementedTypeNames[$iface->name] = true; - $this->validateObjectImplementsInterface($object, $iface); - } - } - private function validateInterfaces(InterfaceType $iface) - { - $possibleTypes = $this->schema->getPossibleTypes($iface); + if (isset($ifaceTypeNames[$iface->name])) { + $this->reportError( + sprintf('Type %s can only implement %s once.', $type->name, $iface->name), + $this->getAllImplementsInterfaceNodes($type, $iface) + ); + continue; + } + $ifaceTypeNames[$iface->name] = true; - if (count($possibleTypes) !== 0) { - return; + $this->validateTypeImplementsAncestors($type, $iface); + $this->validateTypeImplementsInterface($type, $iface); } - - $this->reportError( - sprintf( - 'Interface %s must be implemented by at least one Object type.', - $iface->name - ), - $iface->astNode - ); } /** - * @param InterfaceType $iface + * @param Schema|Type $object * - * @return NamedTypeNode|null + * @return NodeList */ - private function getImplementsInterfaceNode(ObjectType $type, $iface) + private function getDirectives($object) { - $nodes = $this->getAllImplementsInterfaceNodes($type, $iface); + return $this->getAllSubNodes($object, static function ($node) { + return $node->directives; + }); + } + + /** + * @param ObjectType|InterfaceType $type + */ + private function getImplementsInterfaceNode(ImplementingType $type, Type $shouldBeInterface) : ?NamedTypeNode + { + $nodes = $this->getAllImplementsInterfaceNodes($type, $shouldBeInterface); return $nodes[0] ?? null; } /** - * @param InterfaceType $iface + * @param ObjectType|InterfaceType $type * - * @return NamedTypeNode[] + * @return array */ - private function getAllImplementsInterfaceNodes(ObjectType $type, $iface) + private function getAllImplementsInterfaceNodes(ImplementingType $type, Type $shouldBeInterface) : array { - $implementsNodes = []; - $astNodes = $this->getAllObjectOrInterfaceNodes($type); - - foreach ($astNodes as $astNode) { - if (! $astNode || ! $astNode->interfaces) { - continue; - } - - foreach ($astNode->interfaces as $node) { - if ($node->name->value !== $iface->name) { - continue; - } - - $implementsNodes[] = $node; - } - } - - return $implementsNodes; + $subNodes = $this->getAllSubNodes($type, static function (Node $typeNode) : NodeList { + /** @var ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode $typeNode */ + return $typeNode->interfaces; + }); + + return Utils::filter($subNodes, static function (NamedTypeNode $ifaceNode) use ($shouldBeInterface) : bool { + return $ifaceNode->name->value === $shouldBeInterface->name; + }); } /** - * @param InterfaceType $iface + * @param ObjectType|InterfaceType $type */ - private function validateObjectImplementsInterface(ObjectType $object, $iface) + private function validateTypeImplementsInterface(ImplementingType $type, InterfaceType $iface) { - $objectFieldMap = $object->getFields(); - $ifaceFieldMap = $iface->getFields(); + $typeFieldMap = $type->getFields(); + $ifaceFieldMap = $iface->getFields(); // Assert each interface field is implemented. foreach ($ifaceFieldMap as $fieldName => $ifaceField) { - $objectField = array_key_exists($fieldName, $objectFieldMap) - ? $objectFieldMap[$fieldName] + $typeField = array_key_exists($fieldName, $typeFieldMap) + ? $typeFieldMap[$fieldName] : null; - // Assert interface field exists on object. - if (! $objectField) { + // Assert interface field exists on type. + if (! $typeField) { $this->reportError( sprintf( 'Interface field %s.%s expected but %s does not provide it.', $iface->name, $fieldName, - $object->name + $type->name ), - [$this->getFieldNode($iface, $fieldName), $object->astNode] + array_merge( + [$this->getFieldNode($iface, $fieldName)], + $this->getAllNodes($type) + ) ); continue; } - // Assert interface field type is satisfied by object field type, by being + // Assert interface field type is satisfied by type field type, by being // a valid subtype. (covariant) if (! TypeComparators::isTypeSubTypeOf( $this->schema, - $objectField->getType(), + $typeField->getType(), $ifaceField->getType() ) ) { @@ -605,52 +785,52 @@ private function validateObjectImplementsInterface(ObjectType $object, $iface) $iface->name, $fieldName, $ifaceField->getType(), - $object->name, + $type->name, $fieldName, - Utils::printSafe($objectField->getType()) + Utils::printSafe($typeField->getType()) ), [ $this->getFieldTypeNode($iface, $fieldName), - $this->getFieldTypeNode($object, $fieldName), + $this->getFieldTypeNode($type, $fieldName), ] ); } // Assert each interface field arg is implemented. foreach ($ifaceField->args as $ifaceArg) { - $argName = $ifaceArg->name; - $objectArg = null; + $argName = $ifaceArg->name; + $typeArg = null; - foreach ($objectField->args as $arg) { + foreach ($typeField->args as $arg) { if ($arg->name === $argName) { - $objectArg = $arg; + $typeArg = $arg; break; } } - // Assert interface field arg exists on object field. - if (! $objectArg) { + // Assert interface field arg exists on type field. + if (! $typeArg) { $this->reportError( sprintf( 'Interface field argument %s.%s(%s:) expected but %s.%s does not provide it.', $iface->name, $fieldName, $argName, - $object->name, + $type->name, $fieldName ), [ $this->getFieldArgNode($iface, $fieldName, $argName), - $this->getFieldNode($object, $fieldName), + $this->getFieldNode($type, $fieldName), ] ); continue; } - // Assert interface field arg type matches object field arg type. + // Assert interface field arg type matches type field arg type. // (invariant) // TODO: change to contravariant? - if (! TypeComparators::isEqualType($ifaceArg->getType(), $objectArg->getType())) { + if (! TypeComparators::isEqualType($ifaceArg->getType(), $typeArg->getType())) { $this->reportError( sprintf( 'Interface field argument %s.%s(%s:) expects type %s but %s.%s(%s:) is type %s.', @@ -658,14 +838,14 @@ private function validateObjectImplementsInterface(ObjectType $object, $iface) $fieldName, $argName, Utils::printSafe($ifaceArg->getType()), - $object->name, + $type->name, $fieldName, $argName, - Utils::printSafe($objectArg->getType()) + Utils::printSafe($typeArg->getType()) ), [ $this->getFieldArgTypeNode($iface, $fieldName, $argName), - $this->getFieldArgTypeNode($object, $fieldName, $argName), + $this->getFieldArgTypeNode($type, $fieldName, $argName), ] ); } @@ -673,8 +853,8 @@ private function validateObjectImplementsInterface(ObjectType $object, $iface) } // Assert additional arguments must not be required. - foreach ($objectField->args as $objectArg) { - $argName = $objectArg->name; + foreach ($typeField->args as $typeArg) { + $argName = $typeArg->name; $ifaceArg = null; foreach ($ifaceField->args as $arg) { @@ -684,22 +864,21 @@ private function validateObjectImplementsInterface(ObjectType $object, $iface) } } - if ($ifaceArg || ! ($objectArg->getType() instanceof NonNull)) { + if ($ifaceArg || ! $typeArg->isRequired()) { continue; } $this->reportError( sprintf( - 'Object field argument %s.%s(%s:) is of required type %s but is not also provided by the Interface field %s.%s.', - $object->name, + 'Object field %s.%s includes required argument %s that is missing from the Interface field %s.%s.', + $type->name, $fieldName, $argName, - Utils::printSafe($objectArg->getType()), $iface->name, $fieldName ), [ - $this->getFieldArgTypeNode($object, $fieldName, $argName), + $this->getFieldArgNode($type, $fieldName, $argName), $this->getFieldNode($iface, $fieldName), ] ); @@ -707,6 +886,39 @@ private function validateObjectImplementsInterface(ObjectType $object, $iface) } } + /** + * @param ObjectType|InterfaceType $type + */ + private function validateTypeImplementsAncestors(ImplementingType $type, InterfaceType $iface) : void + { + $typeInterfaces = $type->getInterfaces(); + foreach ($iface->getInterfaces() as $transitive) { + if (in_array($transitive, $typeInterfaces, true)) { + continue; + } + + $error = $transitive === $type ? + sprintf( + 'Type %s cannot implement %s because it would create a circular reference.', + $type->name, + $iface->name + ) : + sprintf( + 'Type %s must implement %s because it is implemented by %s.', + $type->name, + $transitive->name, + $iface->name + ); + $this->reportError( + $error, + array_merge( + $this->getAllImplementsInterfaceNodes($iface, $transitive), + $this->getAllImplementsInterfaceNodes($type, $iface) + ) + ); + } + } + private function validateUnionMembers(UnionType $union) { $memberTypes = $union->getTypes(); @@ -714,7 +926,7 @@ private function validateUnionMembers(UnionType $union) if (! $memberTypes) { $this->reportError( sprintf('Union type %s must define one or more member types.', $union->name), - $union->astNode + $this->getAllNodes($union) ); } @@ -751,17 +963,13 @@ private function validateUnionMembers(UnionType $union) */ private function getUnionMemberTypeNodes(UnionType $union, $typeName) { - if ($union->astNode && $union->astNode->types) { - return array_filter( - $union->astNode->types, - static function (NamedTypeNode $value) use ($typeName) { - return $value->name->value === $typeName; - } - ); - } + $subNodes = $this->getAllSubNodes($union, static function ($unionNode) { + return $unionNode->types; + }); - return $union->astNode ? - $union->astNode->types : null; + return Utils::filter($subNodes, static function ($typeNode) use ($typeName) : bool { + return $typeNode->name->value === $typeName; + }); } private function validateEnumValues(EnumType $enumType) @@ -771,7 +979,7 @@ private function validateEnumValues(EnumType $enumType) if (! $enumValues) { $this->reportError( sprintf('Enum type %s must define one or more values.', $enumType->name), - $enumType->astNode + $this->getAllNodes($enumType) ); } @@ -789,13 +997,21 @@ private function validateEnumValues(EnumType $enumType) // Ensure valid name. $this->validateName($enumValue); - if ($valueName !== 'true' && $valueName !== 'false' && $valueName !== 'null') { + if ($valueName === 'true' || $valueName === 'false' || $valueName === 'null') { + $this->reportError( + sprintf('Enum type %s cannot include value: %s.', $enumType->name, $valueName), + $enumValue->astNode + ); + } + + // Ensure valid directives + if (! isset($enumValue->astNode, $enumValue->astNode->directives)) { continue; } - $this->reportError( - sprintf('Enum type %s cannot include value: %s.', $enumType->name, $valueName), - $enumValue->astNode + $this->validateDirectivesAtLocation( + $enumValue->astNode->directives, + DirectiveLocation::ENUM_VALUE ); } } @@ -807,17 +1023,13 @@ private function validateEnumValues(EnumType $enumType) */ private function getEnumValueNodes(EnumType $enum, $valueName) { - if ($enum->astNode && $enum->astNode->values) { - return array_filter( - iterator_to_array($enum->astNode->values), - static function (EnumValueDefinitionNode $value) use ($valueName) { - return $value->name->value === $valueName; - } - ); - } + $subNodes = $this->getAllSubNodes($enum, static function ($enumNode) { + return $enumNode->values; + }); - return $enum->astNode ? - $enum->astNode->values : null; + return Utils::filter($subNodes, static function ($valueNode) use ($valueName) : bool { + return $valueNode->name->value === $valueName; + }); } private function validateInputFields(InputObjectType $inputObj) @@ -827,7 +1039,7 @@ private function validateInputFields(InputObjectType $inputObj) if (! $fieldMap) { $this->reportError( sprintf('Input Object type %s must define one or more fields.', $inputObj->name), - $inputObj->astNode + $this->getAllNodes($inputObj) ); } @@ -839,18 +1051,26 @@ private function validateInputFields(InputObjectType $inputObj) // TODO: Ensure they are unique per field. // Ensure the type is an input type - if (Type::isInputType($field->getType())) { + if (! Type::isInputType($field->getType())) { + $this->reportError( + sprintf( + 'The type of %s.%s must be Input Type but got: %s.', + $inputObj->name, + $fieldName, + Utils::printSafe($field->getType()) + ), + $field->astNode ? $field->astNode->type : null + ); + } + + // Ensure valid directives + if (! isset($field->astNode, $field->astNode->directives)) { continue; } - $this->reportError( - sprintf( - 'The type of %s.%s must be Input Type but got: %s.', - $inputObj->name, - $fieldName, - Utils::printSafe($field->getType()) - ), - $field->astNode ? $field->astNode->type : null + $this->validateDirectivesAtLocation( + $field->astNode->directives, + DirectiveLocation::INPUT_FIELD_DEFINITION ); } } diff --git a/src/Type/TypeKind.php b/src/Type/TypeKind.php index 7cd38ff07..69cdf2e2c 100644 --- a/src/Type/TypeKind.php +++ b/src/Type/TypeKind.php @@ -6,12 +6,12 @@ class TypeKind { - const SCALAR = 0; - const OBJECT = 1; - const INTERFACE_KIND = 2; - const UNION = 3; - const ENUM = 4; - const INPUT_OBJECT = 5; - const LIST_KIND = 6; - const NON_NULL = 7; + const SCALAR = 'SCALAR'; + const OBJECT = 'OBJECT'; + const INTERFACE = 'INTERFACE'; + const UNION = 'UNION'; + const ENUM = 'ENUM'; + const INPUT_OBJECT = 'INPUT_OBJECT'; + const LIST = 'LIST'; + const NON_NULL = 'NON_NULL'; } diff --git a/src/Type/Validation/InputObjectCircularRefs.php b/src/Type/Validation/InputObjectCircularRefs.php new file mode 100644 index 000000000..7291ae42b --- /dev/null +++ b/src/Type/Validation/InputObjectCircularRefs.php @@ -0,0 +1,105 @@ + + */ + private $visitedTypes = []; + + /** @var InputObjectField[] */ + private $fieldPath = []; + + /** + * Position in the type path. + * + * [string $typeName => int $index] + * + * @var int[] + */ + private $fieldPathIndexByTypeName = []; + + public function __construct(SchemaValidationContext $schemaValidationContext) + { + $this->schemaValidationContext = $schemaValidationContext; + } + + /** + * This does a straight-forward DFS to find cycles. + * It does not terminate when a cycle was found but continues to explore + * the graph to find all possible cycles. + */ + public function validate(InputObjectType $inputObj) : void + { + if (isset($this->visitedTypes[$inputObj->name])) { + return; + } + + $this->visitedTypes[$inputObj->name] = true; + $this->fieldPathIndexByTypeName[$inputObj->name] = count($this->fieldPath); + + $fieldMap = $inputObj->getFields(); + foreach ($fieldMap as $fieldName => $field) { + $type = $field->getType(); + + if ($type instanceof NonNull) { + $fieldType = $type->getWrappedType(); + + // If the type of the field is anything else then a non-nullable input object, + // there is no chance of an unbreakable cycle + if ($fieldType instanceof InputObjectType) { + $this->fieldPath[] = $field; + + if (! isset($this->fieldPathIndexByTypeName[$fieldType->name])) { + $this->validate($fieldType); + } else { + $cycleIndex = $this->fieldPathIndexByTypeName[$fieldType->name]; + $cyclePath = array_slice($this->fieldPath, $cycleIndex); + $fieldNames = array_map( + static function (InputObjectField $field) : string { + return $field->name; + }, + $cyclePath + ); + + $this->schemaValidationContext->reportError( + 'Cannot reference Input Object "' . $fieldType->name . '" within itself ' + . 'through a series of non-null fields: "' . implode('.', $fieldNames) . '".', + array_map( + static function (InputObjectField $field) : ?InputValueDefinitionNode { + return $field->astNode; + }, + $cyclePath + ) + ); + } + } + } + + array_pop($this->fieldPath); + } + + unset($this->fieldPathIndexByTypeName[$inputObj->name]); + } +} diff --git a/src/Utils/AST.php b/src/Utils/AST.php index ae01ac5ef..bba62341b 100644 --- a/src/Utils/AST.php +++ b/src/Utils/AST.php @@ -6,6 +6,7 @@ use ArrayAccess; use Exception; +use GraphQL\Error\DebugFlag; use GraphQL\Error\Error; use GraphQL\Error\InvariantViolation; use GraphQL\Language\AST\BooleanValueNode; @@ -105,7 +106,7 @@ public static function fromArray(array $node) : Node continue; } if (is_array($value)) { - if (isset($value[0]) || empty($value)) { + if (isset($value[0]) || count($value) === 0) { $value = new NodeList($value); } else { $value = self::fromArray($value); @@ -124,7 +125,7 @@ public static function fromArray(array $node) : Node * * @api */ - public static function toArray(Node $node) + public static function toArray(Node $node) : array { return $node->toArray(true); } @@ -149,7 +150,7 @@ public static function toArray(Node $node) * * @param Type|mixed|null $value * - * @return ObjectValueNode|ListValueNode|BooleanValueNode|IntValueNode|FloatValueNode|EnumValueNode|StringValueNode|NullValueNode + * @return ObjectValueNode|ListValueNode|BooleanValueNode|IntValueNode|FloatValueNode|EnumValueNode|StringValueNode|NullValueNode|null * * @api */ @@ -183,7 +184,7 @@ public static function astFromValue($value, InputType $type) $valuesNodes[] = $itemNode; } - return new ListValueNode(['values' => $valuesNodes]); + return new ListValueNode(['values' => new NodeList($valuesNodes)]); } return self::astFromValue($value, $itemType); @@ -213,7 +214,6 @@ public static function astFromValue($value, InputType $type) } elseif ($isArray) { $fieldExists = array_key_exists($fieldName, $value); } elseif ($isArrayLike) { - /** @var ArrayAccess $value */ $fieldExists = $value->offsetExists($fieldName); } else { $fieldExists = property_exists($value, $fieldName); @@ -235,7 +235,7 @@ public static function astFromValue($value, InputType $type) ]); } - return new ObjectValueNode(['fields' => $fieldNodes]); + return new ObjectValueNode(['fields' => new NodeList($fieldNodes)]); } if ($type instanceof ScalarType || $type instanceof EnumType) { @@ -243,11 +243,6 @@ public static function astFromValue($value, InputType $type) // to an externally represented value before converting into an AST. try { $serialized = $type->serialize($value); - } catch (Exception $error) { - if ($error instanceof Error && $type instanceof EnumType) { - return null; - } - throw $error; } catch (Throwable $error) { if ($error instanceof Error && $type instanceof EnumType) { return null; @@ -260,16 +255,16 @@ public static function astFromValue($value, InputType $type) return new BooleanValueNode(['value' => $serialized]); } if (is_int($serialized)) { - return new IntValueNode(['value' => $serialized]); + return new IntValueNode(['value' => (string) $serialized]); } if (is_float($serialized)) { // int cast with == used for performance reasons // phpcs:ignore if ((int) $serialized == $serialized) { - return new IntValueNode(['value' => $serialized]); + return new IntValueNode(['value' => (string) $serialized]); } - return new FloatValueNode(['value' => $serialized]); + return new FloatValueNode(['value' => (string) $serialized]); } if (is_string($serialized)) { // Enum types use Enum literals. @@ -313,8 +308,8 @@ public static function astFromValue($value, InputType $type) * | Enum Value | Mixed | * | Null Value | null | * - * @param ValueNode|null $valueNode - * @param mixed[]|null $variables + * @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null $valueNode + * @param mixed[]|null $variables * * @return mixed[]|stdClass|null * @@ -322,7 +317,7 @@ public static function astFromValue($value, InputType $type) * * @api */ - public static function valueFromAST($valueNode, InputType $type, ?array $variables = null) + public static function valueFromAST(?ValueNode $valueNode, Type $type, ?array $variables = null) { $undefined = Utils::undefined(); @@ -354,9 +349,14 @@ public static function valueFromAST($valueNode, InputType $type, ?array $variabl return $undefined; } - // Note: we're not doing any checking that this variable is correct. We're - // assuming that this query has been validated and the variable usage here - // is of the correct type. + $variableValue = $variables[$variableName] ?? null; + if ($variableValue === null && $type instanceof NonNull) { + return $undefined; // Invalid: intentionally return no value. + } + + // Note: This does no further checking that this variable is correct. + // This assumes that this query has been validated and the variable + // usage here is of the correct type. return $variables[$variableName]; } @@ -403,16 +403,18 @@ public static function valueFromAST($valueNode, InputType $type, ?array $variabl } $coercedObj = []; - $fields = $type->getFields(); $fieldNodes = Utils::keyMap( $valueNode->fields, static function ($field) { return $field->name->value; } ); + + $fields = array_merge(array_flip(array_map(function($item) { return $item->name->value; }, $fieldNodes)), $type->getFields()); + foreach ($fields as $field) { - /** @var ValueNode $fieldNode */ $fieldName = $field->name; + /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode $fieldNode */ $fieldNode = $fieldNodes[$fieldName] ?? null; if ($fieldNode === null || self::isMissingVariable($fieldNode->value, $variables)) { @@ -459,8 +461,6 @@ static function ($field) { // no value is returned. try { return $type->parseLiteral($valueNode, $variables); - } catch (Exception $error) { - return $undefined; } catch (Throwable $error) { return $undefined; } @@ -473,12 +473,12 @@ static function ($field) { * Returns true if the provided valueNode is a variable which is not defined * in the set of variables. * - * @param ValueNode $valueNode - * @param mixed[] $variables + * @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode $valueNode + * @param mixed[] $variables * * @return bool */ - private static function isMissingVariable($valueNode, $variables) + private static function isMissingVariable(ValueNode $valueNode, $variables) { return $valueNode instanceof VariableNode && (count($variables) === 0 || ! array_key_exists($valueNode->name->value, $variables)); @@ -515,9 +515,9 @@ public static function valueFromASTUntyped($valueNode, ?array $variables = null) case $valueNode instanceof NullValueNode: return null; case $valueNode instanceof IntValueNode: - return intval($valueNode->value, 10); + return (int) $valueNode->value; case $valueNode instanceof FloatValueNode: - return floatval($valueNode->value); + return (float) $valueNode->value; case $valueNode instanceof StringValueNode: case $valueNode instanceof EnumValueNode: case $valueNode instanceof BooleanValueNode: @@ -532,7 +532,7 @@ static function ($node) use ($variables) { case $valueNode instanceof ObjectValueNode: return array_combine( array_map( - static function ($field) { + static function ($field) : string { return $field->name->value; }, iterator_to_array($valueNode->fields) @@ -590,7 +590,7 @@ public static function typeFromAST(Schema $schema, $inputTypeNode) * * @param string $operationName * - * @return bool + * @return bool|string * * @api */ diff --git a/src/Utils/ASTDefinitionBuilder.php b/src/Utils/ASTDefinitionBuilder.php index 95f8d5ad7..92e7d27a8 100644 --- a/src/Utils/ASTDefinitionBuilder.php +++ b/src/Utils/ASTDefinitionBuilder.php @@ -8,7 +8,6 @@ use GraphQL\Executor\Values; use GraphQL\Language\AST\DirectiveDefinitionNode; use GraphQL\Language\AST\EnumTypeDefinitionNode; -use GraphQL\Language\AST\EnumTypeExtensionNode; use GraphQL\Language\AST\EnumValueDefinitionNode; use GraphQL\Language\AST\FieldDefinitionNode; use GraphQL\Language\AST\InputObjectTypeDefinitionNode; @@ -16,11 +15,13 @@ use GraphQL\Language\AST\InterfaceTypeDefinitionNode; use GraphQL\Language\AST\ListTypeNode; use GraphQL\Language\AST\NamedTypeNode; +use GraphQL\Language\AST\NameNode; use GraphQL\Language\AST\Node; -use GraphQL\Language\AST\NodeKind; +use GraphQL\Language\AST\NodeList; use GraphQL\Language\AST\NonNullTypeNode; use GraphQL\Language\AST\ObjectTypeDefinitionNode; use GraphQL\Language\AST\ScalarTypeDefinitionNode; +use GraphQL\Language\AST\TypeDefinitionNode; use GraphQL\Language\AST\TypeNode; use GraphQL\Language\AST\UnionTypeDefinitionNode; use GraphQL\Language\Token; @@ -29,9 +30,7 @@ use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\InputObjectType; -use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\InterfaceType; -use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; @@ -44,28 +43,30 @@ class ASTDefinitionBuilder { - /** @var Node[] */ + /** @var array */ private $typeDefinitionsMap; /** @var callable */ private $typeConfigDecorator; - /** @var bool[] */ + /** @var array */ private $options; /** @var callable */ private $resolveType; - /** @var Type[] */ + /** @var array */ private $cache; /** - * @param Node[] $typeDefinitionsMap - * @param bool[] $options + * code sniffer doesn't understand this syntax. Pr with a fix here: waiting on https://github.com/squizlabs/PHP_CodeSniffer/pull/2919 + * phpcs:disable Squiz.Commenting.FunctionComment.SpacingAfterParamType + * @param array $typeDefinitionsMap + * @param array $options */ public function __construct( array $typeDefinitionsMap, - $options, + array $options, callable $resolveType, ?callable $typeConfigDecorator = null ) { @@ -77,30 +78,32 @@ public function __construct( $this->cache = Type::getAllBuiltInTypes(); } - public function buildDirective(DirectiveDefinitionNode $directiveNode) + public function buildDirective(DirectiveDefinitionNode $directiveNode) : Directive { return new Directive([ - 'name' => $directiveNode->name->value, - 'description' => $this->getDescription($directiveNode), - 'locations' => Utils::map( + 'name' => $directiveNode->name->value, + 'description' => $this->getDescription($directiveNode), + 'args' => FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)), + 'isRepeatable' => $directiveNode->repeatable, + 'locations' => Utils::map( $directiveNode->locations, - static function ($node) { + static function (NameNode $node) : string { return $node->value; } ), - 'args' => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null, - 'astNode' => $directiveNode, + 'astNode' => $directiveNode, ]); } /** * Given an ast node, returns its string description. */ - private function getDescription($node) + private function getDescription(Node $node) : ?string { - if ($node->description) { + if (isset($node->description)) { return $node->description->value; } + if (isset($this->options['commentDescriptions'])) { $rawValue = $this->getLeadingCommentBlock($node); if ($rawValue !== null) { @@ -111,19 +114,21 @@ private function getDescription($node) return null; } - private function getLeadingCommentBlock($node) + private function getLeadingCommentBlock(Node $node) : ?string { $loc = $node->loc; - if (! $loc || ! $loc->startToken) { + if ($loc === null || $loc->startToken === null) { return null; } + $comments = []; $token = $loc->startToken->prev; - while ($token && - $token->kind === Token::COMMENT && - $token->next && $token->prev && - $token->line + 1 === $token->next->line && - $token->line !== $token->prev->line + while ($token !== null + && $token->kind === Token::COMMENT + && $token->next !== null + && $token->prev !== null + && $token->line + 1 === $token->next->line + && $token->line !== $token->prev->line ) { $value = $token->value; $comments[] = $value; @@ -133,18 +138,21 @@ private function getLeadingCommentBlock($node) return implode("\n", array_reverse($comments)); } - private function makeInputValues($values) + /** + * @return array> + */ + private function makeInputValues(NodeList $values) : array { return Utils::keyValMap( $values, - static function ($value) { + static function (InputValueDefinitionNode $value) : string { return $value->name->value; }, - function ($value) { + function (InputValueDefinitionNode $value) : array { // Note: While this could make assertions to get the correctly typed // value, that would throw immediately while type system validation // with validateSchema() will produce more actionable results. - $type = $this->internalBuildWrappedType($value->type); + $type = $this->buildWrappedType($value->type); $config = [ 'name' => $value->name->value, @@ -161,26 +169,23 @@ function ($value) { ); } - /** - * @return Type|InputType - * - * @throws Error - */ - private function internalBuildWrappedType(TypeNode $typeNode) + private function buildWrappedType(TypeNode $typeNode) : Type { - $typeDef = $this->buildType($this->getNamedTypeNode($typeNode)); + if ($typeNode instanceof ListTypeNode) { + return Type::listOf($this->buildWrappedType($typeNode->type)); + } - return $this->buildWrappedType($typeDef, $typeNode); + if ($typeNode instanceof NonNullTypeNode) { + return Type::nonNull($this->buildWrappedType($typeNode->type)); + } + + return $this->buildType($typeNode); } /** - * @param string|NamedTypeNode $ref - * - * @return Type - * - * @throws Error + * @param string|(Node &NamedTypeNode)|(Node&TypeDefinitionNode) $ref */ - public function buildType($ref) + public function buildType($ref) : Type { if (is_string($ref)) { return $this->internalBuildType($ref); @@ -190,14 +195,11 @@ public function buildType($ref) } /** - * @param string $typeName - * @param NamedTypeNode|null $typeNode - * - * @return Type + * @param (Node &NamedTypeNode)|(Node&TypeDefinitionNode)|null $typeNode * * @throws Error */ - private function internalBuildType($typeName, $typeNode = null) + private function internalBuildType(string $typeName, ?Node $typeNode = null) : Type { if (! isset($this->cache[$typeName])) { if (isset($this->typeDefinitionsMap[$typeName])) { @@ -212,7 +214,7 @@ private function internalBuildType($typeName, $typeNode = null) sprintf('when building %s type: %s', $typeName, $e->getMessage()), null, null, - null, + [], null, $e ); @@ -245,70 +247,71 @@ private function internalBuildType($typeName, $typeNode = null) * * @throws Error */ - private function makeSchemaDef($def) + private function makeSchemaDef(Node $def) : Type { - if (! $def) { - throw new Error('def must be defined.'); - } - switch ($def->kind) { - case NodeKind::OBJECT_TYPE_DEFINITION: + switch (true) { + case $def instanceof ObjectTypeDefinitionNode: return $this->makeTypeDef($def); - case NodeKind::INTERFACE_TYPE_DEFINITION: + case $def instanceof InterfaceTypeDefinitionNode: return $this->makeInterfaceDef($def); - case NodeKind::ENUM_TYPE_DEFINITION: + case $def instanceof EnumTypeDefinitionNode: return $this->makeEnumDef($def); - case NodeKind::UNION_TYPE_DEFINITION: + case $def instanceof UnionTypeDefinitionNode: return $this->makeUnionDef($def); - case NodeKind::SCALAR_TYPE_DEFINITION: + case $def instanceof ScalarTypeDefinitionNode: return $this->makeScalarDef($def); - case NodeKind::INPUT_OBJECT_TYPE_DEFINITION: + case $def instanceof InputObjectTypeDefinitionNode: return $this->makeInputObjectDef($def); default: throw new Error(sprintf('Type kind of %s not supported.', $def->kind)); } } - private function makeTypeDef(ObjectTypeDefinitionNode $def) + private function makeTypeDef(ObjectTypeDefinitionNode $def) : ObjectType { - $typeName = $def->name->value; - return new ObjectType([ - 'name' => $typeName, + 'name' => $def->name->value, 'description' => $this->getDescription($def), - 'fields' => function () use ($def) { + 'fields' => function () use ($def) : array { return $this->makeFieldDefMap($def); }, - 'interfaces' => function () use ($def) { + 'interfaces' => function () use ($def) : array { return $this->makeImplementedInterfaces($def); }, 'astNode' => $def, ]); } - private function makeFieldDefMap($def) + /** + * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode $def + * + * @return array> + */ + private function makeFieldDefMap(Node $def) : array { - return $def->fields - ? Utils::keyValMap( - $def->fields, - static function ($field) { - return $field->name->value; - }, - function ($field) { - return $this->buildField($field); - } - ) - : []; + return Utils::keyValMap( + $def->fields, + static function (FieldDefinitionNode $field) : string { + return $field->name->value; + }, + function (FieldDefinitionNode $field) : array { + return $this->buildField($field); + } + ); } - public function buildField(FieldDefinitionNode $field) + /** + * @return array + */ + public function buildField(FieldDefinitionNode $field) : array { return [ // Note: While this could make assertions to get the correctly typed // value, that would throw immediately while type system validation // with validateSchema() will produce more actionable results. - 'type' => $this->internalBuildWrappedType($field->type), + 'type' => $this->buildWrappedType($field->type), 'description' => $this->getDescription($field), - 'args' => $field->arguments ? $this->makeInputValues($field->arguments) : null, + 'args' => $this->makeInputValues($field->arguments), 'deprecationReason' => $this->getDeprecationReason($field), 'astNode' => $field, ]; @@ -318,73 +321,74 @@ public function buildField(FieldDefinitionNode $field) * Given a collection of directives, returns the string value for the * deprecation reason. * - * @param EnumValueDefinitionNode | FieldDefinitionNode $node - * - * @return string + * @param EnumValueDefinitionNode|FieldDefinitionNode $node */ - private function getDeprecationReason($node) + private function getDeprecationReason(Node $node) : ?string { - $deprecated = Values::getDirectiveValues(Directive::deprecatedDirective(), $node); + $deprecated = Values::getDirectiveValues( + Directive::deprecatedDirective(), + $node + ); return $deprecated['reason'] ?? null; } - private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def) + /** + * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode $def + * + * @return array + */ + private function makeImplementedInterfaces($def) : array { - if ($def->interfaces) { - // Note: While this could make early assertions to get the correctly - // typed values, that would throw immediately while type system - // validation with validateSchema() will produce more actionable results. - return Utils::map( - $def->interfaces, - function ($iface) { - return $this->buildType($iface); - } - ); - } - - return null; + // Note: While this could make early assertions to get the correctly + // typed values, that would throw immediately while type system + // validation with validateSchema() will produce more actionable results. + return Utils::map( + $def->interfaces, + function (NamedTypeNode $iface) : Type { + return $this->buildType($iface); + } + ); } - private function makeInterfaceDef(InterfaceTypeDefinitionNode $def) + private function makeInterfaceDef(InterfaceTypeDefinitionNode $def) : InterfaceType { - $typeName = $def->name->value; - return new InterfaceType([ - 'name' => $typeName, + 'name' => $def->name->value, 'description' => $this->getDescription($def), - 'fields' => function () use ($def) { + 'fields' => function () use ($def) : array { return $this->makeFieldDefMap($def); }, + 'interfaces' => function () use ($def) : array { + return $this->makeImplementedInterfaces($def); + }, 'astNode' => $def, ]); } - private function makeEnumDef(EnumTypeDefinitionNode $def) + private function makeEnumDef(EnumTypeDefinitionNode $def) : EnumType { return new EnumType([ 'name' => $def->name->value, 'description' => $this->getDescription($def), - 'values' => $def->values - ? Utils::keyValMap( - $def->values, - static function ($enumValue) { - return $enumValue->name->value; - }, - function ($enumValue) { - return [ - 'description' => $this->getDescription($enumValue), - 'deprecationReason' => $this->getDeprecationReason($enumValue), - 'astNode' => $enumValue, - ]; - } - ) - : [], + 'values' => Utils::keyValMap( + $def->values, + static function ($enumValue) { + return $enumValue->name->value; + }, + function ($enumValue) : array { + return [ + 'description' => $this->getDescription($enumValue), + 'deprecationReason' => $this->getDeprecationReason($enumValue), + 'astNode' => $enumValue, + ]; + } + ), 'astNode' => $def, ]); } - private function makeUnionDef(UnionTypeDefinitionNode $def) + private function makeUnionDef(UnionTypeDefinitionNode $def) : UnionType { return new UnionType([ 'name' => $def->name->value, @@ -392,19 +396,19 @@ private function makeUnionDef(UnionTypeDefinitionNode $def) // Note: While this could make assertions to get the correctly typed // values below, that would throw immediately while type system // validation with validateSchema() will produce more actionable results. - 'types' => $def->types - ? Utils::map( + 'types' => function () use ($def) : array { + return Utils::map( $def->types, - function ($typeNode) { + function ($typeNode) : Type { return $this->buildType($typeNode); } - ) : - [], + ); + }, 'astNode' => $def, ]); } - private function makeScalarDef(ScalarTypeDefinitionNode $def) + private function makeScalarDef(ScalarTypeDefinitionNode $def) : CustomScalarType { return new CustomScalarType([ 'name' => $def->name->value, @@ -416,45 +420,39 @@ private function makeScalarDef(ScalarTypeDefinitionNode $def) ]); } - private function makeInputObjectDef(InputObjectTypeDefinitionNode $def) + private function makeInputObjectDef(InputObjectTypeDefinitionNode $def) : InputObjectType { return new InputObjectType([ 'name' => $def->name->value, 'description' => $this->getDescription($def), - 'fields' => function () use ($def) { - return $def->fields - ? $this->makeInputValues($def->fields) - : []; + 'fields' => function () use ($def) : array { + return $this->makeInputValues($def->fields); }, 'astNode' => $def, ]); } /** - * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|EnumTypeExtensionNode|ScalarTypeDefinitionNode|InputObjectTypeDefinitionNode $def - * @param mixed[] $config + * @param array $config * * @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType * * @throws Error */ - private function makeSchemaDefFromConfig($def, array $config) + private function makeSchemaDefFromConfig(Node $def, array $config) : Type { - if (! $def) { - throw new Error('def must be defined.'); - } - switch ($def->kind) { - case NodeKind::OBJECT_TYPE_DEFINITION: + switch (true) { + case $def instanceof ObjectTypeDefinitionNode: return new ObjectType($config); - case NodeKind::INTERFACE_TYPE_DEFINITION: + case $def instanceof InterfaceTypeDefinitionNode: return new InterfaceType($config); - case NodeKind::ENUM_TYPE_DEFINITION: + case $def instanceof EnumTypeDefinitionNode: return new EnumType($config); - case NodeKind::UNION_TYPE_DEFINITION: + case $def instanceof UnionTypeDefinitionNode: return new UnionType($config); - case NodeKind::SCALAR_TYPE_DEFINITION: + case $def instanceof ScalarTypeDefinitionNode: return new CustomScalarType($config); - case NodeKind::INPUT_OBJECT_TYPE_DEFINITION: + case $def instanceof InputObjectTypeDefinitionNode: return new InputObjectType($config); default: throw new Error(sprintf('Type kind of %s not supported.', $def->kind)); @@ -462,45 +460,11 @@ private function makeSchemaDefFromConfig($def, array $config) } /** - * @param TypeNode|ListTypeNode|NonNullTypeNode $typeNode - * - * @return TypeNode - */ - private function getNamedTypeNode(TypeNode $typeNode) - { - $namedType = $typeNode; - while ($namedType->kind === NodeKind::LIST_TYPE || $namedType->kind === NodeKind::NON_NULL_TYPE) { - $namedType = $namedType->type; - } - - return $namedType; - } - - /** - * @param TypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode - * - * @return Type - */ - private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode) - { - if ($inputTypeNode->kind === NodeKind::LIST_TYPE) { - return Type::listOf($this->buildWrappedType($innerType, $inputTypeNode->type)); - } - if ($inputTypeNode->kind === NodeKind::NON_NULL_TYPE) { - $wrappedType = $this->buildWrappedType($innerType, $inputTypeNode->type); - - return Type::nonNull(NonNull::assertNullableType($wrappedType)); - } - - return $innerType; - } - - /** - * @return mixed[] + * @return array */ public function buildInputField(InputValueDefinitionNode $value) : array { - $type = $this->internalBuildWrappedType($value->type); + $type = $this->buildWrappedType($value->type); $config = [ 'name' => $value->name->value, @@ -509,7 +473,7 @@ public function buildInputField(InputValueDefinitionNode $value) : array 'astNode' => $value, ]; - if ($value->defaultValue) { + if ($value->defaultValue !== null) { $config['defaultValue'] = $value->defaultValue; } @@ -517,7 +481,7 @@ public function buildInputField(InputValueDefinitionNode $value) : array } /** - * @return mixed[] + * @return array */ public function buildEnumValue(EnumValueDefinitionNode $value) : array { diff --git a/src/Utils/BreakingChangesFinder.php b/src/Utils/BreakingChangesFinder.php index 533abf6f0..234f114d5 100644 --- a/src/Utils/BreakingChangesFinder.php +++ b/src/Utils/BreakingChangesFinder.php @@ -11,6 +11,7 @@ use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\FieldArgument; +use GraphQL\Type\Definition\ImplementingType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ListOfType; @@ -39,19 +40,23 @@ class BreakingChangesFinder public const BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM = 'VALUE_REMOVED_FROM_ENUM'; public const BREAKING_CHANGE_ARG_REMOVED = 'ARG_REMOVED'; public const BREAKING_CHANGE_ARG_CHANGED_KIND = 'ARG_CHANGED_KIND'; - public const BREAKING_CHANGE_NON_NULL_ARG_ADDED = 'NON_NULL_ARG_ADDED'; - public const BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED = 'NON_NULL_INPUT_FIELD_ADDED'; - public const BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT = 'INTERFACE_REMOVED_FROM_OBJECT'; + public const BREAKING_CHANGE_REQUIRED_ARG_ADDED = 'REQUIRED_ARG_ADDED'; + public const BREAKING_CHANGE_REQUIRED_INPUT_FIELD_ADDED = 'REQUIRED_INPUT_FIELD_ADDED'; + public const BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED = 'IMPLEMENTED_INTERFACE_REMOVED'; public const BREAKING_CHANGE_DIRECTIVE_REMOVED = 'DIRECTIVE_REMOVED'; public const BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED = 'DIRECTIVE_ARG_REMOVED'; public const BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED = 'DIRECTIVE_LOCATION_REMOVED'; - public const BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED = 'NON_NULL_DIRECTIVE_ARG_ADDED'; + public const BREAKING_CHANGE_REQUIRED_DIRECTIVE_ARG_ADDED = 'REQUIRED_DIRECTIVE_ARG_ADDED'; public const DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED = 'ARG_DEFAULT_VALUE_CHANGE'; public const DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM = 'VALUE_ADDED_TO_ENUM'; - public const DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT = 'INTERFACE_ADDED_TO_OBJECT'; + public const DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED = 'IMPLEMENTED_INTERFACE_ADDED'; public const DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION = 'TYPE_ADDED_TO_UNION'; - public const DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED = 'NULLABLE_INPUT_FIELD_ADDED'; - public const DANGEROUS_CHANGE_NULLABLE_ARG_ADDED = 'NULLABLE_ARG_ADDED'; + public const DANGEROUS_CHANGE_OPTIONAL_INPUT_FIELD_ADDED = 'OPTIONAL_INPUT_FIELD_ADDED'; + public const DANGEROUS_CHANGE_OPTIONAL_ARG_ADDED = 'OPTIONAL_ARG_ADDED'; + /** @deprecated use BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED instead, will be removed in v15.0.0. */ + public const BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT = 'IMPLEMENTED_INTERFACE_REMOVED'; + /** @deprecated use DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED instead, will be removed in v15.0.0. */ + public const DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT = 'IMPLEMENTED_INTERFACE_ADDED'; /** * Given two schemas, returns an Array containing descriptions of all the types @@ -214,10 +219,10 @@ public static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes( $newFieldType ); if (! $isSafe) { - $oldFieldTypeString = $oldFieldType instanceof NamedType + $oldFieldTypeString = $oldFieldType instanceof NamedType && $oldFieldType instanceof Type ? $oldFieldType->name : $oldFieldType; - $newFieldTypeString = $newFieldType instanceof NamedType + $newFieldTypeString = $newFieldType instanceof NamedType && $newFieldType instanceof Type ? $newFieldType->name : $newFieldType; $breakingChanges[] = [ @@ -270,7 +275,7 @@ private static function isChangeSafeForObjectOrInterfaceField( } /** - * @return string[][] + * @return array>> */ public static function findFieldsThatChangedTypeOnInputObjectTypes( Schema $oldSchema, @@ -304,13 +309,17 @@ public static function findFieldsThatChangedTypeOnInputObjectTypes( $newFieldType ); if (! $isSafe) { - $oldFieldTypeString = $oldFieldType instanceof NamedType - ? $oldFieldType->name - : $oldFieldType; - $newFieldTypeString = $newFieldType instanceof NamedType - ? $newFieldType->name - : $newFieldType; - $breakingChanges[] = [ + if ($oldFieldType instanceof NamedType) { + $oldFieldTypeString = $oldFieldType->name; + } else { + $oldFieldTypeString = $oldFieldType; + } + if ($newFieldType instanceof NamedType) { + $newFieldTypeString = $newFieldType->name; + } else { + $newFieldTypeString = $newFieldType; + } + $breakingChanges[] = [ 'type' => self::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => "${typeName}.${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}.", ]; @@ -324,15 +333,15 @@ public static function findFieldsThatChangedTypeOnInputObjectTypes( } $newTypeName = $newType->name; - if ($fieldDef->getType() instanceof NonNull) { + if ($fieldDef->isRequired()) { $breakingChanges[] = [ - 'type' => self::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED, - 'description' => "A non-null field ${fieldName} on input type ${newTypeName} was added.", + 'type' => self::BREAKING_CHANGE_REQUIRED_INPUT_FIELD_ADDED, + 'description' => "A required field ${fieldName} on input type ${newTypeName} was added.", ]; } else { $dangerousChanges[] = [ - 'type' => self::DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED, - 'description' => "A nullable field ${fieldName} on input type ${newTypeName} was added.", + 'type' => self::DANGEROUS_CHANGE_OPTIONAL_INPUT_FIELD_ADDED, + 'description' => "An optional field ${fieldName} on input type ${newTypeName} was added.", ]; } } @@ -352,8 +361,12 @@ private static function isChangeSafeForInputObjectFieldOrFieldArg( Type $newType ) { if ($oldType instanceof NamedType) { + if (! $newType instanceof NamedType) { + return false; + } + // if they're both named types, see if their names are equivalent - return $newType instanceof NamedType && $oldType->name === $newType->name; + return $oldType->name === $newType->name; } if ($oldType instanceof ListOfType) { @@ -463,7 +476,7 @@ public static function findValuesRemovedFromEnums( * (such as removal or change of type of an argument, or a change in an * argument's default value). * - * @return string[][] + * @return array>> */ public static function findArgChanges( Schema $oldSchema, @@ -496,15 +509,16 @@ public static function findArgChanges( $newArgs = $newTypeFields[$fieldName]->args; $newArgDef = Utils::find( $newArgs, - static function ($arg) use ($oldArgDef) { + static function ($arg) use ($oldArgDef) : bool { return $arg->name === $oldArgDef->name; } ); if ($newArgDef !== null) { - $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg( + $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg( $oldArgDef->getType(), $newArgDef->getType() ); + /** @var ScalarType|EnumType|InputObjectType|ListOfType|NonNull $oldArgType */ $oldArgType = $oldArgDef->getType(); $oldArgName = $oldArgDef->name; if (! $isSafe) { @@ -530,12 +544,12 @@ static function ($arg) use ($oldArgDef) { ), ]; } - // Check if a non-null arg was added to the field + // Check if arg was added to the field foreach ($newTypeFields[$fieldName]->args as $newTypeFieldArgDef) { $oldArgs = $oldTypeFields[$fieldName]->args; $oldArgDef = Utils::find( $oldArgs, - static function ($arg) use ($newTypeFieldArgDef) { + static function ($arg) use ($newTypeFieldArgDef) : bool { return $arg->name === $newTypeFieldArgDef->name; } ); @@ -546,15 +560,15 @@ static function ($arg) use ($newTypeFieldArgDef) { $newTypeName = $newType->name; $newArgName = $newTypeFieldArgDef->name; - if ($newTypeFieldArgDef->getType() instanceof NonNull) { + if ($newTypeFieldArgDef->isRequired()) { $breakingChanges[] = [ - 'type' => self::BREAKING_CHANGE_NON_NULL_ARG_ADDED, - 'description' => "A non-null arg ${newArgName} on ${newTypeName}.${fieldName} was added", + 'type' => self::BREAKING_CHANGE_REQUIRED_ARG_ADDED, + 'description' => "A required arg ${newArgName} on ${newTypeName}.${fieldName} was added", ]; } else { $dangerousChanges[] = [ - 'type' => self::DANGEROUS_CHANGE_NULLABLE_ARG_ADDED, - 'description' => "A nullable arg ${newArgName} on ${newTypeName}.${fieldName} was added", + 'type' => self::DANGEROUS_CHANGE_OPTIONAL_ARG_ADDED, + 'description' => "An optional arg ${newArgName} on ${newTypeName}.${fieldName} was added", ]; } } @@ -581,7 +595,7 @@ public static function findInterfacesRemovedFromObjectTypes( foreach ($oldTypeMap as $typeName => $oldType) { $newType = $newTypeMap[$typeName] ?? null; - if (! ($oldType instanceof ObjectType) || ! ($newType instanceof ObjectType)) { + if (! ($oldType instanceof ImplementingType) || ! ($newType instanceof ImplementingType)) { continue; } @@ -599,7 +613,7 @@ static function (InterfaceType $interface) use ($oldInterface) : bool { } $breakingChanges[] = [ - 'type' => self::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT, + 'type' => self::BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED, 'description' => sprintf('%s no longer implements interface %s.', $typeName, $oldInterface->name), ]; } @@ -682,7 +696,7 @@ public static function findRemovedArgsForDirectives(Directive $oldDirective, Dir private static function getArgumentMapForDirective(Directive $directive) { return Utils::keyMap( - $directive->args ?: [], + $directive->args ?? [], static function ($arg) { return $arg->name; } @@ -703,13 +717,13 @@ public static function findAddedNonNullDirectiveArgs(Schema $oldSchema, Schema $ $oldSchemaDirectiveMap[$newDirective->name], $newDirective ) as $arg) { - if (! $arg->getType() instanceof NonNull) { + if (! $arg->isRequired()) { continue; } $addedNonNullableArgs[] = [ - 'type' => self::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED, + 'type' => self::BREAKING_CHANGE_REQUIRED_DIRECTIVE_ARG_ADDED, 'description' => sprintf( - 'A non-null arg %s on directive %s was added', + 'A required arg %s on directive %s was added', $arg->name, $newDirective->name ), @@ -848,7 +862,8 @@ public static function findInterfacesAddedToObjectTypes( foreach ($newTypeMap as $typeName => $newType) { $oldType = $oldTypeMap[$typeName] ?? null; - if (! ($oldType instanceof ObjectType) || ! ($newType instanceof ObjectType)) { + if (! ($oldType instanceof ObjectType || $oldType instanceof InterfaceType) + || ! ($newType instanceof ObjectType || $newType instanceof InterfaceType)) { continue; } @@ -867,7 +882,7 @@ static function (InterfaceType $interface) use ($newInterface) : bool { } $interfacesAddedToObjectTypes[] = [ - 'type' => self::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT, + 'type' => self::DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED, 'description' => sprintf( '%s added to interfaces implemented by %s.', $newInterface->name, diff --git a/src/Utils/BuildClientSchema.php b/src/Utils/BuildClientSchema.php new file mode 100644 index 000000000..99e8988ee --- /dev/null +++ b/src/Utils/BuildClientSchema.php @@ -0,0 +1,496 @@ + */ + private $introspection; + + /** @var array */ + private $options; + + /** @var array */ + private $typeMap; + + /** + * @param array $introspectionQuery + * @param array $options + */ + public function __construct(array $introspectionQuery, array $options = []) + { + $this->introspection = $introspectionQuery; + $this->options = $options; + } + + /** + * Build a schema for use by client tools. + * + * Given the result of a client running the introspection query, creates and + * returns a \GraphQL\Type\Schema instance which can be then used with all graphql-php + * tools, but cannot be used to execute a query, as introspection does not + * represent the "resolver", "parse" or "serialize" functions or any other + * server-internal mechanisms. + * + * This function expects a complete introspection result. Don't forget to check + * the "errors" field of a server response before calling this function. + * + * Accepts options as a third argument: + * + * - assumeValid: + * When building a schema from a GraphQL service's introspection result, it + * might be safe to assume the schema is valid. Set to true to assume the + * produced schema is valid. + * + * Default: false + * + * @param array $introspectionQuery + * @param array $options + * + * @api + */ + public static function build(array $introspectionQuery, array $options = []) : Schema + { + $builder = new self($introspectionQuery, $options); + + return $builder->buildSchema(); + } + + public function buildSchema() : Schema + { + if (! array_key_exists('__schema', $this->introspection)) { + throw new InvariantViolation('Invalid or incomplete introspection result. Ensure that you are passing "data" property of introspection response and no "errors" was returned alongside: ' . json_encode($this->introspection) . '.'); + } + + $schemaIntrospection = $this->introspection['__schema']; + + $this->typeMap = Utils::keyValMap( + $schemaIntrospection['types'], + static function (array $typeIntrospection) { + return $typeIntrospection['name']; + }, + function (array $typeIntrospection) : NamedType { + return $this->buildType($typeIntrospection); + } + ); + + $builtInTypes = array_merge( + Type::getStandardTypes(), + Introspection::getTypes() + ); + foreach ($builtInTypes as $name => $type) { + if (! isset($this->typeMap[$name])) { + continue; + } + + $this->typeMap[$name] = $type; + } + + $queryType = isset($schemaIntrospection['queryType']) + ? $this->getObjectType($schemaIntrospection['queryType']) + : null; + + $mutationType = isset($schemaIntrospection['mutationType']) + ? $this->getObjectType($schemaIntrospection['mutationType']) + : null; + + $subscriptionType = isset($schemaIntrospection['subscriptionType']) + ? $this->getObjectType($schemaIntrospection['subscriptionType']) + : null; + + $directives = isset($schemaIntrospection['directives']) + ? array_map( + [$this, 'buildDirective'], + $schemaIntrospection['directives'] + ) + : []; + + $schemaConfig = new SchemaConfig(); + $schemaConfig->setQuery($queryType) + ->setMutation($mutationType) + ->setSubscription($subscriptionType) + ->setTypes($this->typeMap) + ->setDirectives($directives) + ->setAssumeValid( + isset($this->options) + && isset($this->options['assumeValid']) + && $this->options['assumeValid'] + ); + + return new Schema($schemaConfig); + } + + /** + * @param array $typeRef + */ + private function getType(array $typeRef) : Type + { + if (isset($typeRef['kind'])) { + if ($typeRef['kind'] === TypeKind::LIST) { + if (! isset($typeRef['ofType'])) { + throw new InvariantViolation('Decorated type deeper than introspection query.'); + } + + return new ListOfType($this->getType($typeRef['ofType'])); + } + + if ($typeRef['kind'] === TypeKind::NON_NULL) { + if (! isset($typeRef['ofType'])) { + throw new InvariantViolation('Decorated type deeper than introspection query.'); + } + /** @var NullableType $nullableType */ + $nullableType = $this->getType($typeRef['ofType']); + + return new NonNull($nullableType); + } + } + + if (! isset($typeRef['name'])) { + throw new InvariantViolation('Unknown type reference: ' . json_encode($typeRef) . '.'); + } + + return $this->getNamedType($typeRef['name']); + } + + /** + * @return NamedType&Type + */ + private function getNamedType(string $typeName) : NamedType + { + if (! isset($this->typeMap[$typeName])) { + throw new InvariantViolation( + "Invalid or incomplete schema, unknown type: ${typeName}. Ensure that a full introspection query is used in order to build a client schema." + ); + } + + return $this->typeMap[$typeName]; + } + + /** + * @param array $typeRef + */ + private function getInputType(array $typeRef) : InputType + { + $type = $this->getType($typeRef); + + if ($type instanceof InputType) { + return $type; + } + + throw new InvariantViolation('Introspection must provide input type for arguments, but received: ' . json_encode($type) . '.'); + } + + /** + * @param array $typeRef + */ + private function getOutputType(array $typeRef) : OutputType + { + $type = $this->getType($typeRef); + + if ($type instanceof OutputType) { + return $type; + } + + throw new InvariantViolation('Introspection must provide output type for fields, but received: ' . json_encode($type) . '.'); + } + + /** + * @param array $typeRef + */ + private function getObjectType(array $typeRef) : ObjectType + { + $type = $this->getType($typeRef); + + return ObjectType::assertObjectType($type); + } + + /** + * @param array $typeRef + */ + public function getInterfaceType(array $typeRef) : InterfaceType + { + $type = $this->getType($typeRef); + + return InterfaceType::assertInterfaceType($type); + } + + /** + * @param array $type + */ + private function buildType(array $type) : NamedType + { + if (array_key_exists('name', $type) && array_key_exists('kind', $type)) { + switch ($type['kind']) { + case TypeKind::SCALAR: + return $this->buildScalarDef($type); + case TypeKind::OBJECT: + return $this->buildObjectDef($type); + case TypeKind::INTERFACE: + return $this->buildInterfaceDef($type); + case TypeKind::UNION: + return $this->buildUnionDef($type); + case TypeKind::ENUM: + return $this->buildEnumDef($type); + case TypeKind::INPUT_OBJECT: + return $this->buildInputObjectDef($type); + } + } + + throw new InvariantViolation( + 'Invalid or incomplete introspection result. Ensure that a full introspection query is used in order to build a client schema: ' . json_encode($type) . '.' + ); + } + + /** + * @param array $scalar + */ + private function buildScalarDef(array $scalar) : ScalarType + { + return new CustomScalarType([ + 'name' => $scalar['name'], + 'description' => $scalar['description'], + 'serialize' => static function ($value) : string { + return (string) $value; + }, + ]); + } + + /** + * @param array $implementingIntrospection + * + * @return array + */ + private function buildImplementationsList(array $implementingIntrospection) : array + { + // TODO: Temporary workaround until GraphQL ecosystem will fully support 'interfaces' on interface types. + if (array_key_exists('interfaces', $implementingIntrospection) && + $implementingIntrospection['interfaces'] === null && + $implementingIntrospection['kind'] === TypeKind::INTERFACE) { + return []; + } + + if (! array_key_exists('interfaces', $implementingIntrospection)) { + throw new InvariantViolation('Introspection result missing interfaces: ' . json_encode($implementingIntrospection) . '.'); + } + + return array_map([$this, 'getInterfaceType'], $implementingIntrospection['interfaces']); + } + + /** + * @param array $object + */ + private function buildObjectDef(array $object) : ObjectType + { + return new ObjectType([ + 'name' => $object['name'], + 'description' => $object['description'], + 'interfaces' => function () use ($object) : array { + return $this->buildImplementationsList($object); + }, + 'fields' => function () use ($object) { + return $this->buildFieldDefMap($object); + }, + ]); + } + + /** + * @param array $interface + */ + private function buildInterfaceDef(array $interface) : InterfaceType + { + return new InterfaceType([ + 'name' => $interface['name'], + 'description' => $interface['description'], + 'fields' => function () use ($interface) { + return $this->buildFieldDefMap($interface); + }, + 'interfaces' => function () use ($interface) : array { + return $this->buildImplementationsList($interface); + }, + ]); + } + + /** + * @param array> $union + */ + private function buildUnionDef(array $union) : UnionType + { + if (! array_key_exists('possibleTypes', $union)) { + throw new InvariantViolation('Introspection result missing possibleTypes: ' . json_encode($union) . '.'); + } + + return new UnionType([ + 'name' => $union['name'], + 'description' => $union['description'], + 'types' => function () use ($union) : array { + return array_map( + [$this, 'getObjectType'], + $union['possibleTypes'] + ); + }, + ]); + } + + /** + * @param array> $enum + */ + private function buildEnumDef(array $enum) : EnumType + { + if (! array_key_exists('enumValues', $enum)) { + throw new InvariantViolation('Introspection result missing enumValues: ' . json_encode($enum) . '.'); + } + + return new EnumType([ + 'name' => $enum['name'], + 'description' => $enum['description'], + 'values' => Utils::keyValMap( + $enum['enumValues'], + static function (array $enumValue) : string { + return $enumValue['name']; + }, + static function (array $enumValue) : array { + return [ + 'description' => $enumValue['description'], + 'deprecationReason' => $enumValue['deprecationReason'], + ]; + } + ), + ]); + } + + /** + * @param array $inputObject + */ + private function buildInputObjectDef(array $inputObject) : InputObjectType + { + if (! array_key_exists('inputFields', $inputObject)) { + throw new InvariantViolation('Introspection result missing inputFields: ' . json_encode($inputObject) . '.'); + } + + return new InputObjectType([ + 'name' => $inputObject['name'], + 'description' => $inputObject['description'], + 'fields' => function () use ($inputObject) : array { + return $this->buildInputValueDefMap($inputObject['inputFields']); + }, + ]); + } + + /** + * @param array $typeIntrospection + */ + private function buildFieldDefMap(array $typeIntrospection) + { + if (! array_key_exists('fields', $typeIntrospection)) { + throw new InvariantViolation('Introspection result missing fields: ' . json_encode($typeIntrospection) . '.'); + } + + return Utils::keyValMap( + $typeIntrospection['fields'], + static function (array $fieldIntrospection) : string { + return $fieldIntrospection['name']; + }, + function (array $fieldIntrospection) : array { + if (! array_key_exists('args', $fieldIntrospection)) { + throw new InvariantViolation('Introspection result missing field args: ' . json_encode($fieldIntrospection) . '.'); + } + + return [ + 'description' => $fieldIntrospection['description'], + 'deprecationReason' => $fieldIntrospection['deprecationReason'], + 'type' => $this->getOutputType($fieldIntrospection['type']), + 'args' => $this->buildInputValueDefMap($fieldIntrospection['args']), + ]; + } + ); + } + + /** + * @param array> $inputValueIntrospections + * + * @return array> + */ + private function buildInputValueDefMap(array $inputValueIntrospections) : array + { + return Utils::keyValMap( + $inputValueIntrospections, + static function (array $inputValue) : string { + return $inputValue['name']; + }, + [$this, 'buildInputValue'] + ); + } + + /** + * @param array $inputValueIntrospection + * + * @return array + */ + public function buildInputValue(array $inputValueIntrospection) : array + { + $type = $this->getInputType($inputValueIntrospection['type']); + + $inputValue = [ + 'description' => $inputValueIntrospection['description'], + 'type' => $type, + ]; + + if (isset($inputValueIntrospection['defaultValue'])) { + $inputValue['defaultValue'] = AST::valueFromAST( + Parser::parseValue($inputValueIntrospection['defaultValue']), + $type + ); + } + + return $inputValue; + } + + /** + * @param array $directive + */ + public function buildDirective(array $directive) : Directive + { + if (! array_key_exists('args', $directive)) { + throw new InvariantViolation('Introspection result missing directive args: ' . json_encode($directive) . '.'); + } + if (! array_key_exists('locations', $directive)) { + throw new InvariantViolation('Introspection result missing directive locations: ' . json_encode($directive) . '.'); + } + + return new Directive([ + 'name' => $directive['name'], + 'description' => $directive['description'], + 'args' => $this->buildInputValueDefMap($directive['args']), + 'isRepeatable' => $directive['isRepeatable'], + 'locations' => $directive['locations'], + ]); + } +} diff --git a/src/Utils/BuildSchema.php b/src/Utils/BuildSchema.php index cf0eb7bda..3e0577547 100644 --- a/src/Utils/BuildSchema.php +++ b/src/Utils/BuildSchema.php @@ -5,16 +5,23 @@ namespace GraphQL\Utils; use GraphQL\Error\Error; +use GraphQL\Language\AST\DirectiveDefinitionNode; use GraphQL\Language\AST\DocumentNode; -use GraphQL\Language\AST\Node; -use GraphQL\Language\AST\NodeKind; +use GraphQL\Language\AST\EnumTypeDefinitionNode; +use GraphQL\Language\AST\InputObjectTypeDefinitionNode; +use GraphQL\Language\AST\InterfaceTypeDefinitionNode; +use GraphQL\Language\AST\ObjectTypeDefinitionNode; +use GraphQL\Language\AST\ScalarTypeDefinitionNode; use GraphQL\Language\AST\SchemaDefinitionNode; +use GraphQL\Language\AST\TypeDefinitionNode; +use GraphQL\Language\AST\UnionTypeDefinitionNode; use GraphQL\Language\Parser; use GraphQL\Language\Source; use GraphQL\Type\Definition\Directive; +use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; +use GraphQL\Validator\DocumentValidator; use function array_map; -use function array_reduce; use function sprintf; /** @@ -26,17 +33,17 @@ class BuildSchema /** @var DocumentNode */ private $ast; - /** @var Node[] */ + /** @var array */ private $nodeMap; /** @var callable|null */ private $typeConfigDecorator; - /** @var bool[] */ + /** @var array */ private $options; /** - * @param bool[] $options + * @param array $options */ public function __construct(DocumentNode $ast, ?callable $typeConfigDecorator = null, array $options = []) { @@ -50,7 +57,7 @@ public function __construct(DocumentNode $ast, ?callable $typeConfigDecorator = * document. * * @param DocumentNode|Source|string $source - * @param bool[] $options + * @param array $options * * @return Schema * @@ -58,7 +65,9 @@ public function __construct(DocumentNode $ast, ?callable $typeConfigDecorator = */ public static function build($source, ?callable $typeConfigDecorator = null, array $options = []) { - $doc = $source instanceof DocumentNode ? $source : Parser::parse($source); + $doc = $source instanceof DocumentNode + ? $source + : Parser::parse($source); return self::buildAST($doc, $typeConfigDecorator, $options); } @@ -77,8 +86,9 @@ public static function build($source, ?callable $typeConfigDecorator = null, arr * * - commentDescriptions: * Provide true to use preceding comments as the description. + * This option is provided to ease adoption and will be removed in v16. * - * @param bool[] $options + * @param array $options * * @return Schema * @@ -95,39 +105,36 @@ public static function buildAST(DocumentNode $ast, ?callable $typeConfigDecorato public function buildSchema() { - /** @var SchemaDefinitionNode $schemaDef */ + $options = $this->options; + if (! ($options['assumeValid'] ?? false) && ! ($options['assumeValidSDL'] ?? false)) { + DocumentValidator::assertValidSDL($this->ast); + } + $schemaDef = null; $typeDefs = []; $this->nodeMap = []; + /** @var array $directiveDefs */ $directiveDefs = []; - foreach ($this->ast->definitions as $d) { - switch ($d->kind) { - case NodeKind::SCHEMA_DEFINITION: - if ($schemaDef) { - throw new Error('Must provide only one schema definition.'); - } - $schemaDef = $d; + foreach ($this->ast->definitions as $definition) { + switch (true) { + case $definition instanceof SchemaDefinitionNode: + $schemaDef = $definition; break; - case NodeKind::SCALAR_TYPE_DEFINITION: - case NodeKind::OBJECT_TYPE_DEFINITION: - case NodeKind::INTERFACE_TYPE_DEFINITION: - case NodeKind::ENUM_TYPE_DEFINITION: - case NodeKind::UNION_TYPE_DEFINITION: - case NodeKind::INPUT_OBJECT_TYPE_DEFINITION: - $typeName = $d->name->value; - if (! empty($this->nodeMap[$typeName])) { + case $definition instanceof TypeDefinitionNode: + $typeName = $definition->name->value; + if (isset($this->nodeMap[$typeName])) { throw new Error(sprintf('Type "%s" was defined more than once.', $typeName)); } - $typeDefs[] = $d; - $this->nodeMap[$typeName] = $d; + $typeDefs[] = $definition; + $this->nodeMap[$typeName] = $definition; break; - case NodeKind::DIRECTIVE_DEFINITION: - $directiveDefs[] = $d; + case $definition instanceof DirectiveDefinitionNode: + $directiveDefs[] = $definition; break; } } - $operationTypes = $schemaDef + $operationTypes = $schemaDef !== null ? $this->getOperationTypes($schemaDef) : [ 'query' => isset($this->nodeMap['Query']) ? 'Query' : null, @@ -138,47 +145,33 @@ public function buildSchema() $DefinitionBuilder = new ASTDefinitionBuilder( $this->nodeMap, $this->options, - static function ($typeName) { + static function ($typeName) : void { throw new Error('Type "' . $typeName . '" not found in document.'); }, $this->typeConfigDecorator ); $directives = array_map( - static function ($def) use ($DefinitionBuilder) { + static function (DirectiveDefinitionNode $def) use ($DefinitionBuilder) : Directive { return $DefinitionBuilder->buildDirective($def); }, $directiveDefs ); // If specified directives were not explicitly declared, add them. - $skip = array_reduce( + $directivesByName = Utils::groupBy( $directives, - static function ($hasSkip, $directive) { - return $hasSkip || $directive->name === 'skip'; + static function (Directive $directive) : string { + return $directive->name; } ); - if (! $skip) { + if (! isset($directivesByName['skip'])) { $directives[] = Directive::skipDirective(); } - - $include = array_reduce( - $directives, - static function ($hasInclude, $directive) { - return $hasInclude || $directive->name === 'include'; - } - ); - if (! $include) { + if (! isset($directivesByName['include'])) { $directives[] = Directive::includeDirective(); } - - $deprecated = array_reduce( - $directives, - static function ($hasDeprecated, $directive) { - return $hasDeprecated || $directive->name === 'deprecated'; - } - ); - if (! $deprecated) { + if (! isset($directivesByName['deprecated'])) { $directives[] = Directive::deprecatedDirective(); } @@ -196,13 +189,14 @@ static function ($hasDeprecated, $directive) { 'subscription' => isset($operationTypes['subscription']) ? $DefinitionBuilder->buildType($operationTypes['subscription']) : null, - 'typeLoader' => static function ($name) use ($DefinitionBuilder) { + 'typeLoader' => static function ($name) use ($DefinitionBuilder) : Type { return $DefinitionBuilder->buildType($name); }, 'directives' => $directives, 'astNode' => $schemaDef, - 'types' => function () use ($DefinitionBuilder) { + 'types' => function () use ($DefinitionBuilder) : array { $types = []; + /** @var ScalarTypeDefinitionNode|ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|UnionTypeDefinitionNode|EnumTypeDefinitionNode|InputObjectTypeDefinitionNode $def */ foreach ($this->nodeMap as $name => $def) { $types[] = $DefinitionBuilder->buildType($def->name->value); } diff --git a/src/Utils/InterfaceImplementations.php b/src/Utils/InterfaceImplementations.php new file mode 100644 index 000000000..eca7fd262 --- /dev/null +++ b/src/Utils/InterfaceImplementations.php @@ -0,0 +1,48 @@ + */ + private $objects; + + /** @var array */ + private $interfaces; + + /** + * @param array $objects + * @param array $interfaces + */ + public function __construct(array $objects, array $interfaces) + { + $this->objects = $objects; + $this->interfaces = $interfaces; + } + + /** + * @return array + */ + public function objects() : array + { + return $this->objects; + } + + /** + * @return array + */ + public function interfaces() : array + { + return $this->interfaces; + } +} diff --git a/src/Utils/MixedStore.php b/src/Utils/MixedStore.php index 469abaa9c..8513fd569 100644 --- a/src/Utils/MixedStore.php +++ b/src/Utils/MixedStore.php @@ -21,7 +21,7 @@ * Similar to PHP array, but allows any type of data to act as key (including arrays, objects, scalars) * * Note: unfortunately when storing array as key - access and modification is O(N) - * (yet this should be really rare case and should be avoided when possible) + * (yet this should rarely be the case and should be avoided when possible) */ class MixedStore implements ArrayAccess { diff --git a/src/Utils/SchemaExtender.php b/src/Utils/SchemaExtender.php index 07a1447cc..d3934c245 100644 --- a/src/Utils/SchemaExtender.php +++ b/src/Utils/SchemaExtender.php @@ -7,24 +7,29 @@ use GraphQL\Error\Error; use GraphQL\Language\AST\DirectiveDefinitionNode; use GraphQL\Language\AST\DocumentNode; +use GraphQL\Language\AST\EnumTypeExtensionNode; +use GraphQL\Language\AST\InputObjectTypeExtensionNode; +use GraphQL\Language\AST\InterfaceTypeExtensionNode; use GraphQL\Language\AST\Node; -use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\ObjectTypeExtensionNode; use GraphQL\Language\AST\SchemaDefinitionNode; use GraphQL\Language\AST\SchemaTypeExtensionNode; use GraphQL\Language\AST\TypeDefinitionNode; use GraphQL\Language\AST\TypeExtensionNode; +use GraphQL\Language\AST\UnionTypeExtensionNode; use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumValueDefinition; use GraphQL\Type\Definition\FieldArgument; +use GraphQL\Type\Definition\ImplementingType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NamedType; use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Introspection; @@ -75,8 +80,8 @@ protected static function getExtensionASTNodes(NamedType $type) : ?array */ protected static function checkExtensionNode(Type $type, Node $node) : void { - switch ($node->kind) { - case NodeKind::OBJECT_TYPE_EXTENSION: + switch (true) { + case $node instanceof ObjectTypeExtensionNode: if (! ($type instanceof ObjectType)) { throw new Error( 'Cannot extend non-object type "' . $type->name . '".', @@ -84,7 +89,7 @@ protected static function checkExtensionNode(Type $type, Node $node) : void ); } break; - case NodeKind::INTERFACE_TYPE_EXTENSION: + case $node instanceof InterfaceTypeExtensionNode: if (! ($type instanceof InterfaceType)) { throw new Error( 'Cannot extend non-interface type "' . $type->name . '".', @@ -92,7 +97,7 @@ protected static function checkExtensionNode(Type $type, Node $node) : void ); } break; - case NodeKind::ENUM_TYPE_EXTENSION: + case $node instanceof EnumTypeExtensionNode: if (! ($type instanceof EnumType)) { throw new Error( 'Cannot extend non-enum type "' . $type->name . '".', @@ -100,7 +105,7 @@ protected static function checkExtensionNode(Type $type, Node $node) : void ); } break; - case NodeKind::UNION_TYPE_EXTENSION: + case $node instanceof UnionTypeExtensionNode: if (! ($type instanceof UnionType)) { throw new Error( 'Cannot extend non-union type "' . $type->name . '".', @@ -108,7 +113,7 @@ protected static function checkExtensionNode(Type $type, Node $node) : void ); } break; - case NodeKind::INPUT_OBJECT_TYPE_EXTENSION: + case $node instanceof InputObjectTypeExtensionNode: if (! ($type instanceof InputObjectType)) { throw new Error( 'Cannot extend non-input object type "' . $type->name . '".', @@ -119,7 +124,7 @@ protected static function checkExtensionNode(Type $type, Node $node) : void } } - protected static function extendCustomScalarType(CustomScalarType $type) : CustomScalarType + protected static function extendScalarType(ScalarType $type) : CustomScalarType { return new CustomScalarType([ 'name' => $type->name, @@ -137,7 +142,7 @@ protected static function extendUnionType(UnionType $type) : UnionType return new UnionType([ 'name' => $type->name, 'description' => $type->description, - 'types' => static function () use ($type) { + 'types' => static function () use ($type) : array { return static::extendPossibleTypes($type); }, 'astNode' => $type->astNode, @@ -162,7 +167,7 @@ protected static function extendInputObjectType(InputObjectType $type) : InputOb return new InputObjectType([ 'name' => $type->name, 'description' => $type->description, - 'fields' => static function () use ($type) { + 'fields' => static function () use ($type) : array { return static::extendInputFieldMap($type); }, 'astNode' => $type->astNode, @@ -180,7 +185,7 @@ protected static function extendInputFieldMap(InputObjectType $type) : array foreach ($oldFieldMap as $fieldName => $field) { $newFieldMap[$fieldName] = [ 'description' => $field->description, - 'type' => static::extendType($field->type), + 'type' => static::extendType($field->getType()), 'astNode' => $field->astNode, ]; @@ -268,9 +273,11 @@ protected static function extendPossibleTypes(UnionType $type) : array } /** - * @return InterfaceType[] + * @param ObjectType|InterfaceType $type + * + * @return array */ - protected static function extendImplementedInterfaces(ObjectType $type) : array + protected static function extendImplementedInterfaces(ImplementingType $type) : array { $interfaces = array_map(static function (InterfaceType $interfaceType) { return static::extendNamedType($interfaceType); @@ -278,7 +285,7 @@ protected static function extendImplementedInterfaces(ObjectType $type) : array $extensions = static::$typeExtensionsMap[$type->name] ?? null; if ($extensions !== null) { - /** @var ObjectTypeExtensionNode $extension */ + /** @var ObjectTypeExtensionNode|InterfaceTypeExtensionNode $extension */ foreach ($extensions as $extension) { foreach ($extension->interfaces as $namedType) { $interfaces[] = static::$astBuilder->buildType($namedType); @@ -292,7 +299,7 @@ protected static function extendImplementedInterfaces(ObjectType $type) : array protected static function extendType($typeDef) { if ($typeDef instanceof ListOfType) { - return Type::listOf(static::extendType($typeDef->ofType)); + return Type::listOf(static::extendType($typeDef->getOfType())); } if ($typeDef instanceof NonNull) { @@ -311,10 +318,10 @@ protected static function extendArgs(array $args) : array { return Utils::keyValMap( $args, - static function (FieldArgument $arg) { + static function (FieldArgument $arg) : string { return $arg->name; }, - static function (FieldArgument $arg) { + static function (FieldArgument $arg) : array { $def = [ 'type' => static::extendType($arg->getType()), 'description' => $arg->description, @@ -378,15 +385,16 @@ protected static function extendObjectType(ObjectType $type) : ObjectType return new ObjectType([ 'name' => $type->name, 'description' => $type->description, - 'interfaces' => static function () use ($type) { + 'interfaces' => static function () use ($type) : array { return static::extendImplementedInterfaces($type); }, - 'fields' => static function () use ($type) { + 'fields' => static function () use ($type) : array { return static::extendFieldMap($type); }, 'astNode' => $type->astNode, 'extensionASTNodes' => static::getExtensionASTNodes($type), 'isTypeOf' => $type->config['isTypeOf'] ?? null, + 'resolveField' => $type->resolveFieldFn ?? null, ]); } @@ -395,7 +403,10 @@ protected static function extendInterfaceType(InterfaceType $type) : InterfaceTy return new InterfaceType([ 'name' => $type->name, 'description' => $type->description, - 'fields' => static function () use ($type) { + 'interfaces' => static function () use ($type) : array { + return static::extendImplementedInterfaces($type); + }, + 'fields' => static function () use ($type) : array { return static::extendFieldMap($type); }, 'astNode' => $type->astNode, @@ -424,8 +435,8 @@ protected static function extendNamedType(Type $type) $name = $type->name; if (! isset(static::$extendTypeCache[$name])) { - if ($type instanceof CustomScalarType) { - static::$extendTypeCache[$name] = static::extendCustomScalarType($type); + if ($type instanceof ScalarType) { + static::$extendTypeCache[$name] = static::extendScalarType($type); } elseif ($type instanceof ObjectType) { static::$extendTypeCache[$name] = static::extendObjectType($type); } elseif ($type instanceof InterfaceType) { @@ -461,7 +472,7 @@ protected static function extendMaybeNamedType(?NamedType $type = null) */ protected static function getMergedDirectives(Schema $schema, array $directiveDefinitions) : array { - $existingDirectives = array_map(static function (Directive $directive) { + $existingDirectives = array_map(static function (Directive $directive) : Directive { return static::extendDirective($directive); }, $schema->getDirectives()); @@ -469,7 +480,7 @@ protected static function getMergedDirectives(Schema $schema, array $directiveDe return array_merge( $existingDirectives, - array_map(static function (DirectiveDefinitionNode $directive) { + array_map(static function (DirectiveDefinitionNode $directive) : Directive { return static::$astBuilder->buildDirective($directive); }, $directiveDefinitions) ); @@ -487,20 +498,21 @@ protected static function extendDirective(Directive $directive) : Directive } /** - * @param mixed[]|null $options + * @param array $options */ - public static function extend(Schema $schema, DocumentNode $documentAST, ?array $options = null) : Schema + public static function extend(Schema $schema, DocumentNode $documentAST, array $options = []) : Schema { - if ($options === null || ! (isset($options['assumeValid']) || isset($options['assumeValidSDL']))) { + if (! (isset($options['assumeValid']) || isset($options['assumeValidSDL']))) { DocumentValidator::assertValidSDLExtension($documentAST, $schema); } + /** @var array $typeDefinitionMap */ $typeDefinitionMap = []; static::$typeExtensionsMap = []; $directiveDefinitions = []; /** @var SchemaDefinitionNode|null $schemaDef */ $schemaDef = null; - /** @var SchemaTypeExtensionNode[] $schemaExtensions */ + /** @var array $schemaExtensions */ $schemaExtensions = []; $definitionsCount = count($documentAST->definitions); @@ -547,11 +559,11 @@ public static function extend(Schema $schema, DocumentNode $documentAST, ?array } } - if (count(static::$typeExtensionsMap) === 0 && - count($typeDefinitionMap) === 0 && - count($directiveDefinitions) === 0 && - count($schemaExtensions) === 0 && - $schemaDef === null + if (count(static::$typeExtensionsMap) === 0 + && count($typeDefinitionMap) === 0 + && count($directiveDefinitions) === 0 + && count($schemaExtensions) === 0 + && $schemaDef === null ) { return $schema; } @@ -560,7 +572,7 @@ public static function extend(Schema $schema, DocumentNode $documentAST, ?array $typeDefinitionMap, $options, static function (string $typeName) use ($schema) { - /** @var NamedType $existingType */ + /** @var ScalarType|ObjectType|InterfaceType|UnionType|EnumType|InputObjectType $existingType */ $existingType = $schema->getType($typeName); if ($existingType !== null) { return static::extendNamedType($existingType); @@ -592,7 +604,7 @@ static function (string $typeName) use ($schema) { } foreach ($schemaExtensions as $schemaExtension) { - if (! $schemaExtension->operationTypes) { + if ($schemaExtension->operationTypes === null) { continue; } @@ -605,17 +617,18 @@ static function (string $typeName) use ($schema) { } } - $schemaExtensionASTNodes = count($schemaExtensions) > 0 - ? ($schema->extensionASTNodes ? array_merge($schema->extensionASTNodes, $schemaExtensions) : $schemaExtensions) - : $schema->extensionASTNodes; + $schemaExtensionASTNodes = array_merge($schema->extensionASTNodes, $schemaExtensions); $types = array_merge( - array_map(static function ($type) { - return static::extendType($type); - }, array_values($schema->getTypeMap())), - array_map(static function ($type) { + // Iterate through all types, getting the type definition for each, ensuring + // that any type not directly referenced by a field will get created. + array_map(static function (Type $type) : Type { + return static::extendNamedType($type); + }, $schema->getTypeMap()), + // Do the same with new types. + array_map(static function (TypeDefinitionNode $type) : Type { return static::$astBuilder->buildType($type); - }, array_values($typeDefinitionMap)) + }, $typeDefinitionMap) ); return new Schema([ diff --git a/src/Utils/SchemaPrinter.php b/src/Utils/SchemaPrinter.php index 9e9e1ca9b..c5f958cef 100644 --- a/src/Utils/SchemaPrinter.php +++ b/src/Utils/SchemaPrinter.php @@ -38,23 +38,22 @@ class SchemaPrinter { /** - * Accepts options as a second argument: - * + * @param array $options + * Available options: * - commentDescriptions: * Provide true to use preceding comments as the description. - * - * @param bool[] $options + * This option is provided to ease adoption and will be removed in v16. * * @api */ public static function doPrint(Schema $schema, array $options = []) : string { - return self::printFilteredSchema( + return static::printFilteredSchema( $schema, - static function ($type) { + static function ($type) : bool { return ! Directive::isSpecifiedDirective($type); }, - static function ($type) { + static function ($type) : bool { return ! Type::isBuiltInType($type); }, $options @@ -62,16 +61,11 @@ static function ($type) { } /** - * @param bool[] $options + * @param array $options */ - private static function printFilteredSchema(Schema $schema, $directiveFilter, $typeFilter, $options) : string + protected static function printFilteredSchema(Schema $schema, callable $directiveFilter, callable $typeFilter, array $options) : string { - $directives = array_filter( - $schema->getDirectives(), - static function ($directive) use ($directiveFilter) { - return $directiveFilter($directive); - } - ); + $directives = array_filter($schema->getDirectives(), $directiveFilter); $types = $schema->getTypeMap(); ksort($types); @@ -83,16 +77,16 @@ static function ($directive) use ($directiveFilter) { "\n\n", array_filter( array_merge( - [self::printSchemaDefinition($schema)], + [static::printSchemaDefinition($schema)], array_map( - static function ($directive) use ($options) { - return self::printDirective($directive, $options); + static function (Directive $directive) use ($options) : string { + return static::printDirective($directive, $options); }, $directives ), array_map( - static function ($type) use ($options) { - return self::printType($type, $options); + static function ($type) use ($options) : string { + return static::printType($type, $options); }, $types ) @@ -102,26 +96,26 @@ static function ($type) use ($options) { ); } - private static function printSchemaDefinition(Schema $schema) + protected static function printSchemaDefinition(Schema $schema) : string { - if (self::isSchemaOfCommonNames($schema)) { - return; + if (static::isSchemaOfCommonNames($schema)) { + return ''; } $operationTypes = []; $queryType = $schema->getQueryType(); - if ($queryType) { + if ($queryType !== null) { $operationTypes[] = sprintf(' query: %s', $queryType->name); } $mutationType = $schema->getMutationType(); - if ($mutationType) { + if ($mutationType !== null) { $operationTypes[] = sprintf(' mutation: %s', $mutationType->name); } $subscriptionType = $schema->getSubscriptionType(); - if ($subscriptionType) { + if ($subscriptionType !== null) { $operationTypes[] = sprintf(' subscription: %s', $subscriptionType->name); } @@ -140,38 +134,46 @@ private static function printSchemaDefinition(Schema $schema) * * When using this naming convention, the schema description can be omitted. */ - private static function isSchemaOfCommonNames(Schema $schema) + protected static function isSchemaOfCommonNames(Schema $schema) : bool { $queryType = $schema->getQueryType(); - if ($queryType && $queryType->name !== 'Query') { + if ($queryType !== null && $queryType->name !== 'Query') { return false; } $mutationType = $schema->getMutationType(); - if ($mutationType && $mutationType->name !== 'Mutation') { + if ($mutationType !== null && $mutationType->name !== 'Mutation') { return false; } $subscriptionType = $schema->getSubscriptionType(); - return ! $subscriptionType || $subscriptionType->name === 'Subscription'; + return $subscriptionType === null || $subscriptionType->name === 'Subscription'; } - private static function printDirective($directive, $options) : string + /** + * @param array $options + */ + protected static function printDirective(Directive $directive, array $options) : string { - return self::printDescription($options, $directive) . - 'directive @' . $directive->name . self::printArgs($options, $directive->args) . - ' on ' . implode(' | ', $directive->locations); + return static::printDescription($options, $directive) + . 'directive @' . $directive->name + . static::printArgs($options, $directive->args) + . ($directive->isRepeatable ? ' repeatable' : '') + . ' on ' . implode(' | ', $directive->locations); } - private static function printDescription($options, $def, $indentation = '', $firstInBlock = true) : string + /** + * @param array $options + */ + protected static function printDescription(array $options, $def, $indentation = '', $firstInBlock = true) : string { if (! $def->description) { return ''; } - $lines = self::descriptionLines($def->description, 120 - strlen($indentation)); + $lines = static::descriptionLines($def->description, 120 - strlen($indentation)); if (isset($options['commentDescriptions'])) { - return self::printDescriptionWithComments($lines, $indentation, $firstInBlock); + return static::printDescriptionWithComments($lines, $indentation, $firstInBlock); } $description = $indentation && ! $firstInBlock @@ -183,7 +185,7 @@ private static function printDescription($options, $def, $indentation = '', $fir mb_strlen($lines[0]) < 70 && substr($lines[0], -1) !== '"' ) { - return $description . self::escapeQuote($lines[0]) . "\"\"\"\n"; + return $description . static::escapeQuote($lines[0]) . "\"\"\"\n"; } // Format a multi-line block quote to account for leading space. @@ -201,7 +203,7 @@ private static function printDescription($options, $def, $indentation = '', $fir if ($i !== 0 || ! $hasLeadingSpace) { $description .= $indentation; } - $description .= self::escapeQuote($lines[$i]) . "\n"; + $description .= static::escapeQuote($lines[$i]) . "\n"; } $description .= $indentation . "\"\"\"\n"; @@ -211,7 +213,7 @@ private static function printDescription($options, $def, $indentation = '', $fir /** * @return string[] */ - private static function descriptionLines(string $description, int $maxLen) : array + protected static function descriptionLines(string $description, int $maxLen) : array { $lines = []; $rawLines = explode("\n", $description); @@ -221,7 +223,7 @@ private static function descriptionLines(string $description, int $maxLen) : arr } else { // For > 120 character long lines, cut at space boundaries into sublines // of ~80 chars. - $sublines = self::breakLine($line, $maxLen); + $sublines = static::breakLine($line, $maxLen); foreach ($sublines as $subline) { $lines[] = $subline; } @@ -234,7 +236,7 @@ private static function descriptionLines(string $description, int $maxLen) : arr /** * @return string[] */ - private static function breakLine(string $line, int $maxLen) : array + protected static function breakLine(string $line, int $maxLen) : array { if (strlen($line) < $maxLen + 5) { return [$line]; @@ -245,7 +247,7 @@ private static function breakLine(string $line, int $maxLen) : array return array_map('trim', $parts); } - private static function printDescriptionWithComments($lines, $indentation, $firstInBlock) : string + protected static function printDescriptionWithComments($lines, $indentation, $firstInBlock) : string { $description = $indentation && ! $firstInBlock ? "\n" : ''; foreach ($lines as $line) { @@ -259,12 +261,15 @@ private static function printDescriptionWithComments($lines, $indentation, $firs return $description; } - private static function escapeQuote($line) : string + protected static function escapeQuote($line) : string { return str_replace('"""', '\\"""', $line); } - private static function printArgs($options, $args, $indentation = '') : string + /** + * @param array $options + */ + protected static function printArgs(array $options, $args, $indentation = '') : string { if (! $args) { return ''; @@ -273,11 +278,11 @@ private static function printArgs($options, $args, $indentation = '') : string // If every arg does not have a description, print them on one line. if (Utils::every( $args, - static function ($arg) { - return empty($arg->description); + static function ($arg) : bool { + return strlen($arg->description ?? '') === 0; } )) { - return '(' . implode(', ', array_map('self::printInputValue', $args)) . ')'; + return '(' . implode(', ', array_map('static::printInputValue', $args)) . ')'; } return sprintf( @@ -285,9 +290,9 @@ static function ($arg) { implode( "\n", array_map( - static function ($arg, $i) use ($indentation, $options) { - return self::printDescription($options, $arg, ' ' . $indentation, ! $i) . ' ' . $indentation . - self::printInputValue($arg); + static function ($arg, $i) use ($indentation, $options) : string { + return static::printDescription($options, $arg, ' ' . $indentation, ! $i) . ' ' . $indentation . + static::printInputValue($arg); }, $args, array_keys($args) @@ -297,7 +302,7 @@ static function ($arg, $i) use ($indentation, $options) { ); } - private static function printInputValue($arg) : string + protected static function printInputValue($arg) : string { $argDecl = $arg->name . ': ' . (string) $arg->getType(); if ($arg->defaultValueExists()) { @@ -308,80 +313,81 @@ private static function printInputValue($arg) : string } /** - * @param bool[] $options + * @param array $options */ public static function printType(Type $type, array $options = []) : string { if ($type instanceof ScalarType) { - return self::printScalar($type, $options); + return static::printScalar($type, $options); } if ($type instanceof ObjectType) { - return self::printObject($type, $options); + return static::printObject($type, $options); } if ($type instanceof InterfaceType) { - return self::printInterface($type, $options); + return static::printInterface($type, $options); } if ($type instanceof UnionType) { - return self::printUnion($type, $options); + return static::printUnion($type, $options); } if ($type instanceof EnumType) { - return self::printEnum($type, $options); + return static::printEnum($type, $options); } if ($type instanceof InputObjectType) { - return self::printInputObject($type, $options); + return static::printInputObject($type, $options); } throw new Error(sprintf('Unknown type: %s.', Utils::printSafe($type))); } /** - * @param bool[] $options + * @param array $options */ - private static function printScalar(ScalarType $type, array $options) : string + protected static function printScalar(ScalarType $type, array $options) : string { - return sprintf('%sscalar %s', self::printDescription($options, $type), $type->name); + return sprintf('%sscalar %s', static::printDescription($options, $type), $type->name); } /** - * @param bool[] $options + * @param array $options */ - private static function printObject(ObjectType $type, array $options) : string + protected static function printObject(ObjectType $type, array $options) : string { $interfaces = $type->getInterfaces(); - $implementedInterfaces = ! empty($interfaces) ? - ' implements ' . implode( + $implementedInterfaces = count($interfaces) > 0 + ? ' implements ' . implode( ' & ', array_map( - static function ($i) { - return $i->name; + static function (InterfaceType $interface) : string { + return $interface->name; }, $interfaces ) - ) : ''; + ) + : ''; - return self::printDescription($options, $type) . - sprintf("type %s%s {\n%s\n}", $type->name, $implementedInterfaces, self::printFields($options, $type)); + return static::printDescription($options, $type) . + sprintf("type %s%s {\n%s\n}", $type->name, $implementedInterfaces, static::printFields($options, $type)); } /** - * @param bool[] $options + * @param array $options */ - private static function printFields($options, $type) : string + protected static function printFields(array $options, $type) : string { $fields = array_values($type->getFields()); return implode( "\n", array_map( - static function ($f, $i) use ($options) { - return self::printDescription($options, $f, ' ', ! $i) . ' ' . - $f->name . self::printArgs($options, $f->args, ' ') . ': ' . - (string) $f->getType() . self::printDeprecated($f); + static function ($f, $i) use ($options) : string { + return static::printDescription($options, $f, ' ', ! $i) . ' ' . + $f->name . static::printArgs($options, $f->args, ' ') . ': ' . + (string) $f->getType() . static::printDeprecated($f); }, $fields, array_keys($fields) @@ -389,10 +395,10 @@ static function ($f, $i) use ($options) { ); } - private static function printDeprecated($fieldOrEnumVal) : string + protected static function printDeprecated($fieldOrEnumVal) : string { $reason = $fieldOrEnumVal->deprecationReason; - if (empty($reason)) { + if ($reason === null) { return ''; } if ($reason === '' || $reason === Directive::DEFAULT_DEPRECATION_REASON) { @@ -404,43 +410,56 @@ private static function printDeprecated($fieldOrEnumVal) : string } /** - * @param bool[] $options + * @param array $options */ - private static function printInterface(InterfaceType $type, array $options) : string + protected static function printInterface(InterfaceType $type, array $options) : string { - return self::printDescription($options, $type) . - sprintf("interface %s {\n%s\n}", $type->name, self::printFields($options, $type)); + $interfaces = $type->getInterfaces(); + $implementedInterfaces = count($interfaces) > 0 + ? ' implements ' . implode( + ' & ', + array_map( + static function (InterfaceType $interface) : string { + return $interface->name; + }, + $interfaces + ) + ) + : ''; + + return static::printDescription($options, $type) . + sprintf("interface %s%s {\n%s\n}", $type->name, $implementedInterfaces, static::printFields($options, $type)); } /** - * @param bool[] $options + * @param array $options */ - private static function printUnion(UnionType $type, array $options) : string + protected static function printUnion(UnionType $type, array $options) : string { - return self::printDescription($options, $type) . + return static::printDescription($options, $type) . sprintf('union %s = %s', $type->name, implode(' | ', $type->getTypes())); } /** - * @param bool[] $options + * @param array $options */ - private static function printEnum(EnumType $type, array $options) : string + protected static function printEnum(EnumType $type, array $options) : string { - return self::printDescription($options, $type) . - sprintf("enum %s {\n%s\n}", $type->name, self::printEnumValues($type->getValues(), $options)); + return static::printDescription($options, $type) . + sprintf("enum %s {\n%s\n}", $type->name, static::printEnumValues($type->getValues(), $options)); } /** - * @param bool[] $options + * @param array $options */ - private static function printEnumValues($values, $options) : string + protected static function printEnumValues($values, array $options) : string { return implode( "\n", array_map( - static function ($value, $i) use ($options) { - return self::printDescription($options, $value, ' ', ! $i) . ' ' . - $value->name . self::printDeprecated($value); + static function ($value, $i) use ($options) : string { + return static::printDescription($options, $value, ' ', ! $i) . ' ' . + $value->name . static::printDeprecated($value); }, $values, array_keys($values) @@ -449,21 +468,21 @@ static function ($value, $i) use ($options) { } /** - * @param bool[] $options + * @param array $options */ - private static function printInputObject(InputObjectType $type, array $options) : string + protected static function printInputObject(InputObjectType $type, array $options) : string { $fields = array_values($type->getFields()); - return self::printDescription($options, $type) . + return static::printDescription($options, $type) . sprintf( "input %s {\n%s\n}", $type->name, implode( "\n", array_map( - static function ($f, $i) use ($options) { - return self::printDescription($options, $f, ' ', ! $i) . ' ' . self::printInputValue($f); + static function ($f, $i) use ($options) : string { + return static::printDescription($options, $f, ' ', ! $i) . ' ' . static::printInputValue($f); }, $fields, array_keys($fields) @@ -473,13 +492,13 @@ static function ($f, $i) use ($options) { } /** - * @param bool[] $options + * @param array $options * * @api */ public static function printIntrospectionSchema(Schema $schema, array $options = []) : string { - return self::printFilteredSchema( + return static::printFilteredSchema( $schema, [Directive::class, 'isSpecifiedDirective'], [Introspection::class, 'isIntrospectionType'], diff --git a/src/Utils/TypeComparators.php b/src/Utils/TypeComparators.php index 2beb942f7..7033eee70 100644 --- a/src/Utils/TypeComparators.php +++ b/src/Utils/TypeComparators.php @@ -6,6 +6,7 @@ use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\CompositeType; +use GraphQL\Type\Definition\ImplementingType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; @@ -44,12 +45,9 @@ public static function isEqualType(Type $typeA, Type $typeB) * Provided a type and a super type, return true if the first type is either * equal or a subset of the second super type (covariant). * - * @param AbstractType $maybeSubType - * @param AbstractType $superType - * * @return bool */ - public static function isTypeSubTypeOf(Schema $schema, $maybeSubType, $superType) + public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType) { // Equivalent type is a valid subtype if ($maybeSubType === $superType) { @@ -85,10 +83,10 @@ public static function isTypeSubTypeOf(Schema $schema, $maybeSubType, $superType } // If superType type is an abstract type, maybeSubType type may be a currently - // possible object type. + // possible object or interface type. return Type::isAbstractType($superType) && - $maybeSubType instanceof ObjectType && - $schema->isPossibleType( + $maybeSubType instanceof ImplementingType && + $schema->isSubType( $superType, $maybeSubType ); @@ -117,7 +115,7 @@ public static function doTypesOverlap(Schema $schema, CompositeType $typeA, Comp // If both types are abstract, then determine if there is any intersection // between possible concrete types of each. foreach ($schema->getPossibleTypes($typeA) as $type) { - if ($schema->isPossibleType($typeB, $type)) { + if ($schema->isSubType($typeB, $type)) { return true; } } @@ -126,12 +124,12 @@ public static function doTypesOverlap(Schema $schema, CompositeType $typeA, Comp } // Determine if the latter type is a possible concrete type of the former. - return $schema->isPossibleType($typeA, $typeB); + return $schema->isSubType($typeA, $typeB); } if ($typeB instanceof AbstractType) { // Determine if the former type is a possible concrete type of the latter. - return $schema->isPossibleType($typeB, $typeA); + return $schema->isSubType($typeB, $typeA); } // Otherwise the types do not overlap. diff --git a/src/Utils/TypeInfo.php b/src/Utils/TypeInfo.php index 89d4ea1a4..7ae3a880e 100644 --- a/src/Utils/TypeInfo.php +++ b/src/Utils/TypeInfo.php @@ -5,18 +5,27 @@ namespace GraphQL\Utils; use GraphQL\Error\InvariantViolation; -use GraphQL\Error\Warning; +use GraphQL\Language\AST\ArgumentNode; +use GraphQL\Language\AST\DirectiveNode; +use GraphQL\Language\AST\EnumValueNode; use GraphQL\Language\AST\FieldNode; +use GraphQL\Language\AST\FragmentDefinitionNode; +use GraphQL\Language\AST\InlineFragmentNode; use GraphQL\Language\AST\ListTypeNode; +use GraphQL\Language\AST\ListValueNode; use GraphQL\Language\AST\NamedTypeNode; use GraphQL\Language\AST\Node; -use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NonNullTypeNode; +use GraphQL\Language\AST\ObjectFieldNode; +use GraphQL\Language\AST\OperationDefinitionNode; +use GraphQL\Language\AST\SelectionSetNode; +use GraphQL\Language\AST\VariableDefinitionNode; use GraphQL\Type\Definition\CompositeType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\FieldDefinition; +use GraphQL\Type\Definition\ImplementingType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\InterfaceType; @@ -28,7 +37,6 @@ use GraphQL\Type\Definition\WrappingType; use GraphQL\Type\Introspection; use GraphQL\Type\Schema; -use SplStack; use function array_map; use function array_merge; use function array_pop; @@ -41,22 +49,25 @@ class TypeInfo /** @var Schema */ private $schema; - /** @var SplStack */ + /** @var array<(OutputType&Type)|null> */ private $typeStack; - /** @var SplStack */ + /** @var array<(CompositeType&Type)|null> */ private $parentTypeStack; - /** @var SplStack */ + /** @var array<(InputType&Type)|null> */ private $inputTypeStack; - /** @var SplStack */ + /** @var array */ private $fieldDefStack; - /** @var Directive */ + /** @var array */ + private $defaultValueStack; + + /** @var Directive|null */ private $directive; - /** @var FieldArgument */ + /** @var FieldArgument|null */ private $argument; /** @var mixed */ @@ -67,12 +78,14 @@ class TypeInfo */ public function __construct(Schema $schema, $initialType = null) { - $this->schema = $schema; - $this->typeStack = []; - $this->parentTypeStack = []; - $this->inputTypeStack = []; - $this->fieldDefStack = []; - if (! $initialType) { + $this->schema = $schema; + $this->typeStack = []; + $this->parentTypeStack = []; + $this->inputTypeStack = []; + $this->fieldDefStack = []; + $this->defaultValueStack = []; + + if ($initialType === null) { return; } @@ -91,14 +104,18 @@ public function __construct(Schema $schema, $initialType = null) /** * @deprecated moved to GraphQL\Utils\TypeComparators + * + * @codeCoverageIgnore */ - public static function isEqualType(Type $typeA, Type $typeB) + public static function isEqualType(Type $typeA, Type $typeB) : bool { return TypeComparators::isEqualType($typeA, $typeB); } /** * @deprecated moved to GraphQL\Utils\TypeComparators + * + * @codeCoverageIgnore */ public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType) { @@ -107,6 +124,8 @@ public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type /** * @deprecated moved to GraphQL\Utils\TypeComparators + * + * @codeCoverageIgnore */ public static function doTypesOverlap(Schema $schema, CompositeType $typeA, CompositeType $typeB) { @@ -141,6 +160,7 @@ public static function extractTypes($type, ?array $typeMap = null) if ($type instanceof WrappingType) { return self::extractTypes($type->getWrappedType(true), $typeMap); } + if (! $type instanceof Type) { // Preserve these invalid types in map (at numeric index) to make them // detectable during $schema->validate() @@ -157,7 +177,7 @@ public static function extractTypes($type, ?array $typeMap = null) return $typeMap; } - if (! empty($typeMap[$type->name])) { + if (isset($typeMap[$type->name])) { Utils::invariant( $typeMap[$type->name] === $type, sprintf('Schema must contain unique named types but contains multiple types named "%s" ', $type) . @@ -173,14 +193,14 @@ public static function extractTypes($type, ?array $typeMap = null) if ($type instanceof UnionType) { $nestedTypes = $type->getTypes(); } - if ($type instanceof ObjectType) { + if ($type instanceof ImplementingType) { $nestedTypes = array_merge($nestedTypes, $type->getInterfaces()); } if ($type instanceof ObjectType || $type instanceof InterfaceType) { foreach ($type->getFields() as $fieldName => $field) { - if (! empty($field->args)) { + if (count($field->args ?? []) > 0) { $fieldArgTypes = array_map( - static function (FieldArgument $arg) { + static function (FieldArgument $arg) : Type { return $arg->getType(); }, $field->args @@ -220,20 +240,14 @@ public static function extractTypesFromDirectives(Directive $directive, array $t } /** - * @return InputType|null + * @return (Type&InputType)|null */ - public function getParentInputType() + public function getParentInputType() : ?InputType { - $inputTypeStackLength = count($this->inputTypeStack); - if ($inputTypeStackLength > 1) { - return $this->inputTypeStack[$inputTypeStackLength - 2]; - } + return $this->inputTypeStack[count($this->inputTypeStack) - 2] ?? null; } - /** - * @return FieldArgument|null - */ - public function getArgument() + public function getArgument() : ?FieldArgument { return $this->argument; } @@ -254,13 +268,13 @@ public function enter(Node $node) // any assumptions of a valid schema to ensure runtime types are properly // checked before continuing since TypeInfo is used as part of validation // which occurs before guarantees of schema and document validity. - switch ($node->kind) { - case NodeKind::SELECTION_SET: + switch (true) { + case $node instanceof SelectionSetNode: $namedType = Type::getNamedType($this->getType()); $this->parentTypeStack[] = Type::isCompositeType($namedType) ? $namedType : null; break; - case NodeKind::FIELD: + case $node instanceof FieldNode: $parentType = $this->getParentType(); $fieldDef = null; if ($parentType) { @@ -274,11 +288,11 @@ public function enter(Node $node) $this->typeStack[] = Type::isOutputType($fieldType) ? $fieldType : null; break; - case NodeKind::DIRECTIVE: + case $node instanceof DirectiveNode: $this->directive = $schema->getDirective($node->name->value); break; - case NodeKind::OPERATION_DEFINITION: + case $node instanceof OperationDefinitionNode: $type = null; if ($node->operation === 'query') { $type = $schema->getQueryType(); @@ -290,28 +304,31 @@ public function enter(Node $node) $this->typeStack[] = Type::isOutputType($type) ? $type : null; break; - case NodeKind::INLINE_FRAGMENT: - case NodeKind::FRAGMENT_DEFINITION: + case $node instanceof InlineFragmentNode: + case $node instanceof FragmentDefinitionNode: $typeConditionNode = $node->typeCondition; - $outputType = $typeConditionNode ? self::typeFromAST( - $schema, - $typeConditionNode - ) : Type::getNamedType($this->getType()); + $outputType = $typeConditionNode + ? self::typeFromAST( + $schema, + $typeConditionNode + ) + : Type::getNamedType($this->getType()); $this->typeStack[] = Type::isOutputType($outputType) ? $outputType : null; break; - case NodeKind::VARIABLE_DEFINITION: + case $node instanceof VariableDefinitionNode: $inputType = self::typeFromAST($schema, $node->type); $this->inputTypeStack[] = Type::isInputType($inputType) ? $inputType : null; // push break; - case NodeKind::ARGUMENT: - $fieldOrDirective = $this->getDirective() ?: $this->getFieldDef(); + case $node instanceof ArgumentNode: + $fieldOrDirective = $this->getDirective() ?? $this->getFieldDef(); $argDef = $argType = null; if ($fieldOrDirective) { + /** @var FieldArgument $argDef */ $argDef = Utils::find( $fieldOrDirective->args, - static function ($arg) use ($node) { + static function ($arg) use ($node) : bool { return $arg->name === $node->name->value; } ); @@ -319,35 +336,41 @@ static function ($arg) use ($node) { $argType = $argDef->getType(); } } - $this->argument = $argDef; - $this->inputTypeStack[] = Type::isInputType($argType) ? $argType : null; + $this->argument = $argDef; + $this->defaultValueStack[] = $argDef && $argDef->defaultValueExists() ? $argDef->defaultValue : Utils::undefined(); + $this->inputTypeStack[] = Type::isInputType($argType) ? $argType : null; break; - case NodeKind::LST: - $listType = Type::getNullableType($this->getInputType()); - $itemType = $listType instanceof ListOfType + case $node instanceof ListValueNode: + $type = $this->getInputType(); + $listType = $type === null ? null : Type::getNullableType($type); + $itemType = $listType instanceof ListOfType ? $listType->getWrappedType() : $listType; - $this->inputTypeStack[] = Type::isInputType($itemType) ? $itemType : null; + // List positions never have a default value. + $this->defaultValueStack[] = Utils::undefined(); + $this->inputTypeStack[] = Type::isInputType($itemType) ? $itemType : null; break; - case NodeKind::OBJECT_FIELD: + case $node instanceof ObjectFieldNode: $objectType = Type::getNamedType($this->getInputType()); $fieldType = null; + $inputField = null; $inputFieldType = null; if ($objectType instanceof InputObjectType) { $tmp = $objectType->getFields(); $inputField = $tmp[$node->name->value] ?? null; $inputFieldType = $inputField ? $inputField->getType() : null; } - $this->inputTypeStack[] = Type::isInputType($inputFieldType) ? $inputFieldType : null; + $this->defaultValueStack[] = $inputField && $inputField->defaultValueExists() ? $inputField->defaultValue : Utils::undefined(); + $this->inputTypeStack[] = Type::isInputType($inputFieldType) ? $inputFieldType : null; break; - case NodeKind::ENUM: + case $node instanceof EnumValueNode: $enumType = Type::getNamedType($this->getInputType()); $enumValue = null; if ($enumType instanceof EnumType) { - $enumValue = $enumType->getValue($node->value); + $this->enumValue = $enumType->getValue($node->value); } $this->enumValue = $enumValue; break; @@ -355,37 +378,27 @@ static function ($arg) use ($node) { } /** - * @return Type + * @return (Type & OutputType) | null */ - public function getType() + public function getType() : ?OutputType { - if (! empty($this->typeStack)) { - return $this->typeStack[count($this->typeStack) - 1]; - } - - return null; + return $this->typeStack[count($this->typeStack) - 1] ?? null; } /** - * @return Type + * @return (CompositeType & Type) | null */ - public function getParentType() + public function getParentType() : ?CompositeType { - if (! empty($this->parentTypeStack)) { - return $this->parentTypeStack[count($this->parentTypeStack) - 1]; - } - - return null; + return $this->parentTypeStack[count($this->parentTypeStack) - 1] ?? null; } /** * Not exactly the same as the executor's definition of getFieldDef, in this * statically evaluated environment we do not always have an Object type, * and need to handle Interface and Union types. - * - * @return FieldDefinition */ - private static function getFieldDefinition(Schema $schema, Type $parentType, FieldNode $fieldNode) + private static function getFieldDefinition(Schema $schema, Type $parentType, FieldNode $fieldNode) : ?FieldDefinition { $name = $fieldNode->name->value; $schemaMeta = Introspection::schemaMetaFieldDef(); @@ -414,80 +427,74 @@ private static function getFieldDefinition(Schema $schema, Type $parentType, Fie /** * @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode * - * @return Type|null - * * @throws InvariantViolation */ - public static function typeFromAST(Schema $schema, $inputTypeNode) + public static function typeFromAST(Schema $schema, $inputTypeNode) : ?Type { return AST::typeFromAST($schema, $inputTypeNode); } - /** - * @return Directive|null - */ - public function getDirective() + public function getDirective() : ?Directive { return $this->directive; } + public function getFieldDef() : ?FieldDefinition + { + return $this->fieldDefStack[count($this->fieldDefStack) - 1] ?? null; + } + /** - * @return FieldDefinition + * @return mixed|null */ - public function getFieldDef() + public function getDefaultValue() { - if (! empty($this->fieldDefStack)) { - return $this->fieldDefStack[count($this->fieldDefStack) - 1]; - } - - return null; + return $this->defaultValueStack[count($this->defaultValueStack) - 1] ?? null; } /** - * @return InputType + * @return (Type & InputType) | null */ - public function getInputType() + public function getInputType() : ?InputType { - if (! empty($this->inputTypeStack)) { - return $this->inputTypeStack[count($this->inputTypeStack) - 1]; - } - - return null; + return $this->inputTypeStack[count($this->inputTypeStack) - 1] ?? null; } public function leave(Node $node) { - switch ($node->kind) { - case NodeKind::SELECTION_SET: + switch (true) { + case $node instanceof SelectionSetNode: array_pop($this->parentTypeStack); break; - case NodeKind::FIELD: + case $node instanceof FieldNode: array_pop($this->fieldDefStack); array_pop($this->typeStack); break; - case NodeKind::DIRECTIVE: + case $node instanceof DirectiveNode: $this->directive = null; break; - case NodeKind::OPERATION_DEFINITION: - case NodeKind::INLINE_FRAGMENT: - case NodeKind::FRAGMENT_DEFINITION: + case $node instanceof OperationDefinitionNode: + case $node instanceof InlineFragmentNode: + case $node instanceof FragmentDefinitionNode: array_pop($this->typeStack); break; - case NodeKind::VARIABLE_DEFINITION: + case $node instanceof VariableDefinitionNode: array_pop($this->inputTypeStack); break; - case NodeKind::ARGUMENT: + case $node instanceof ArgumentNode: $this->argument = null; + array_pop($this->defaultValueStack); array_pop($this->inputTypeStack); break; - case NodeKind::LST: - case NodeKind::OBJECT_FIELD: + case $node instanceof ListValueNode: + case $node instanceof ObjectFieldNode: + array_pop($this->defaultValueStack); array_pop($this->inputTypeStack); break; - case NodeKind::ENUM: + case $node instanceof EnumValueNode: $this->enumValue = null; break; } diff --git a/src/Utils/Utils.php b/src/Utils/Utils.php index 8dddd6563..5811ac0af 100644 --- a/src/Utils/Utils.php +++ b/src/Utils/Utils.php @@ -23,7 +23,6 @@ use function array_slice; use function array_values; use function asort; -use function chr; use function count; use function dechex; use function func_get_args; @@ -59,7 +58,7 @@ public static function undefined() { static $undefined; - return $undefined ?: $undefined = new stdClass(); + return $undefined ?? $undefined = new stdClass(); } /** @@ -104,18 +103,18 @@ public static function assign($obj, array $vars, array $requiredKeys = []) } /** - * @param mixed|Traversable $traversable + * @param iterable $iterable * * @return mixed|null */ - public static function find($traversable, callable $predicate) + public static function find($iterable, callable $predicate) { self::invariant( - is_array($traversable) || $traversable instanceof Traversable, + is_array($iterable) || $iterable instanceof Traversable, __METHOD__ . ' expects array or Traversable' ); - foreach ($traversable as $key => $value) { + foreach ($iterable as $key => $value) { if ($predicate($value, $key)) { return $value; } @@ -125,22 +124,22 @@ public static function find($traversable, callable $predicate) } /** - * @param mixed|Traversable $traversable + * @param iterable $iterable * - * @return mixed[] + * @return array * * @throws Exception */ - public static function filter($traversable, callable $predicate) + public static function filter($iterable, callable $predicate) : array { self::invariant( - is_array($traversable) || $traversable instanceof Traversable, + is_array($iterable) || $iterable instanceof Traversable, __METHOD__ . ' expects array or Traversable' ); $result = []; $assoc = false; - foreach ($traversable as $key => $value) { + foreach ($iterable as $key => $value) { if (! $assoc && ! is_int($key)) { $assoc = true; } @@ -155,21 +154,21 @@ public static function filter($traversable, callable $predicate) } /** - * @param mixed|Traversable $traversable + * @param iterable $iterable * - * @return mixed[] + * @return array * * @throws Exception */ - public static function map($traversable, callable $fn) + public static function map($iterable, callable $fn) : array { self::invariant( - is_array($traversable) || $traversable instanceof Traversable, + is_array($iterable) || $iterable instanceof Traversable, __METHOD__ . ' expects array or Traversable' ); $map = []; - foreach ($traversable as $key => $value) { + foreach ($iterable as $key => $value) { $map[$key] = $fn($value, $key); } @@ -177,21 +176,21 @@ public static function map($traversable, callable $fn) } /** - * @param mixed|Traversable $traversable + * @param iterable $iterable * - * @return mixed[] + * @return array * * @throws Exception */ - public static function mapKeyValue($traversable, callable $fn) + public static function mapKeyValue($iterable, callable $fn) : array { self::invariant( - is_array($traversable) || $traversable instanceof Traversable, + is_array($iterable) || $iterable instanceof Traversable, __METHOD__ . ' expects array or Traversable' ); $map = []; - foreach ($traversable as $key => $value) { + foreach ($iterable as $key => $value) { [$newKey, $newValue] = $fn($value, $key); $map[$newKey] = $newValue; } @@ -200,21 +199,21 @@ public static function mapKeyValue($traversable, callable $fn) } /** - * @param mixed|Traversable $traversable + * @param iterable $iterable * - * @return mixed[] + * @return array * * @throws Exception */ - public static function keyMap($traversable, callable $keyFn) + public static function keyMap($iterable, callable $keyFn) : array { self::invariant( - is_array($traversable) || $traversable instanceof Traversable, + is_array($iterable) || $iterable instanceof Traversable, __METHOD__ . ' expects array or Traversable' ); $map = []; - foreach ($traversable as $key => $value) { + foreach ($iterable as $key => $value) { $newKey = $keyFn($value, $key); if (! is_scalar($newKey)) { continue; @@ -226,20 +225,23 @@ public static function keyMap($traversable, callable $keyFn) return $map; } - public static function each($traversable, callable $fn) + /** + * @param iterable $iterable + */ + public static function each($iterable, callable $fn) : void { self::invariant( - is_array($traversable) || $traversable instanceof Traversable, + is_array($iterable) || $iterable instanceof Traversable, __METHOD__ . ' expects array or Traversable' ); - foreach ($traversable as $key => $item) { + foreach ($iterable as $key => $item) { $fn($item, $key); } } /** - * Splits original traversable to several arrays with keys equal to $keyFn return + * Splits original iterable to several arrays with keys equal to $keyFn return * * E.g. Utils::groupBy([1, 2, 3, 4, 5], function($value) {return $value % 3}) will output: * [ @@ -250,19 +252,19 @@ public static function each($traversable, callable $fn) * * $keyFn is also allowed to return array of keys. Then value will be added to all arrays with given keys * - * @param mixed[]|Traversable $traversable + * @param iterable $iterable * - * @return mixed[] + * @return array> */ - public static function groupBy($traversable, callable $keyFn) + public static function groupBy($iterable, callable $keyFn) : array { self::invariant( - is_array($traversable) || $traversable instanceof Traversable, + is_array($iterable) || $iterable instanceof Traversable, __METHOD__ . ' expects array or Traversable' ); $grouped = []; - foreach ($traversable as $key => $value) { + foreach ($iterable as $key => $value) { $newKeys = (array) $keyFn($value, $key); foreach ($newKeys as $newKey) { $grouped[$newKey][] = $value; @@ -273,14 +275,14 @@ public static function groupBy($traversable, callable $keyFn) } /** - * @param mixed[]|Traversable $traversable + * @param iterable $iterable * - * @return mixed[][] + * @return array */ - public static function keyValMap($traversable, callable $keyFn, callable $valFn) + public static function keyValMap($iterable, callable $keyFn, callable $valFn) : array { $map = []; - foreach ($traversable as $item) { + foreach ($iterable as $item) { $map[$keyFn($item)] = $valFn($item); } @@ -288,13 +290,11 @@ public static function keyValMap($traversable, callable $keyFn, callable $valFn) } /** - * @param mixed[] $traversable - * - * @return bool + * @param iterable $iterable */ - public static function every($traversable, callable $predicate) + public static function every($iterable, callable $predicate) : bool { - foreach ($traversable as $key => $value) { + foreach ($iterable as $key => $value) { if (! $predicate($value, $key)) { return false; } @@ -303,6 +303,20 @@ public static function every($traversable, callable $predicate) return true; } + /** + * @param iterable $iterable + */ + public static function some($iterable, callable $predicate) : bool + { + foreach ($iterable as $key => $value) { + if ($predicate($value, $key)) { + return true; + } + } + + return false; + } + /** * @param bool $test * @param string $message @@ -426,9 +440,6 @@ public static function printSafe($var) */ public static function chr($ord, $encoding = 'UTF-8') { - if ($ord <= 255) { - return chr($ord); - } if ($encoding === 'UCS-4BE') { return pack('N', $ord); } @@ -549,7 +560,7 @@ public static function withErrorHandling(callable $fn, array &$errors) { return static function () use ($fn, &$errors) { // Catch custom errors (to report them in query results) - set_error_handler(static function ($severity, $message, $file, $line) use (&$errors) { + set_error_handler(static function ($severity, $message, $file, $line) use (&$errors) : void { $errors[] = new ErrorException($message, 0, $severity, $file, $line); }); @@ -569,7 +580,7 @@ public static function withErrorHandling(callable $fn, array &$errors) public static function quotedOrList(array $items) { $items = array_map( - static function ($item) { + static function ($item) : string { return sprintf('"%s"', $item); }, $items @@ -598,7 +609,7 @@ public static function orList(array $items) return array_reduce( range(1, $selectedLength - 1), - static function ($list, $index) use ($selected, $selectedLength) { + static function ($list, $index) use ($selected, $selectedLength) : string { return $list . ($selectedLength > 2 ? ', ' : ' ') . ($index === $selectedLength - 1 ? 'or ' : '') . @@ -624,7 +635,7 @@ static function ($list, $index) use ($selected, $selectedLength) { public static function suggestionList($input, array $options) { $optionsByDistance = []; - $inputThreshold = mb_strlen($input) / 2; + $threshold = mb_strlen($input) * 0.4 + 1; foreach ($options as $option) { if ($input === $option) { $distance = 0; @@ -633,7 +644,6 @@ public static function suggestionList($input, array $options) ? 1 : levenshtein($input, $option)); } - $threshold = max($inputThreshold, mb_strlen($option) / 2, 1); if ($distance > $threshold) { continue; } diff --git a/src/Utils/Value.php b/src/Utils/Value.php index 2bb053cda..636abe22e 100644 --- a/src/Utils/Value.php +++ b/src/Utils/Value.php @@ -13,6 +13,7 @@ use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ScalarType; +use stdClass; use Throwable; use Traversable; use function array_key_exists; @@ -35,7 +36,8 @@ class Value /** * Given a type and any value, return a runtime value coerced to match the type. * - * @param mixed[] $path + * @param ScalarType|EnumType|InputObjectType|ListOfType|NonNull $type + * @param mixed[] $path */ public static function coerceValue($value, InputType $type, $blameNode = null, ?array $path = null) { @@ -64,16 +66,6 @@ public static function coerceValue($value, InputType $type, $blameNode = null, ? // the original error. try { return self::ofValue($type->parseValue($value)); - } catch (Exception $error) { - return self::ofErrors([ - self::coercionError( - sprintf('Expected type %s', $type->name), - $blameNode, - $path, - $error->getMessage(), - $error - ), - ]); } catch (Throwable $error) { return self::ofErrors([ self::coercionError( @@ -98,7 +90,7 @@ public static function coerceValue($value, InputType $type, $blameNode = null, ? $suggestions = Utils::suggestionList( Utils::printSafe($value), array_map( - static function ($enumValue) { + static function ($enumValue) : string { return $enumValue->name; }, $type->getValues() @@ -157,6 +149,11 @@ static function ($enumValue) { ]); } + // Cast \stdClass to associative array before checking the fields. Note that the coerced value will be an array. + if ($value instanceof stdClass) { + $value = (array) $value; + } + $errors = []; $coercedValue = []; $fields = $type->getFields(); @@ -184,7 +181,7 @@ static function ($enumValue) { sprintf( 'Field %s of required type %s was not provided', $fieldPath, - $field->type->toString() + $field->getType()->toString() ), $blameNode ) @@ -199,7 +196,7 @@ static function ($enumValue) { } $suggestions = Utils::suggestionList( - $fieldName, + (string) $fieldName, array_keys($fields) ); $didYouMean = $suggestions @@ -252,7 +249,7 @@ private static function coercionError( ($subMessage ? '; ' . $subMessage : '.'), $blameNode, null, - null, + [], null, $originalError ); diff --git a/src/Validator/ASTValidationContext.php b/src/Validator/ASTValidationContext.php new file mode 100644 index 000000000..0b1f1bc78 --- /dev/null +++ b/src/Validator/ASTValidationContext.php @@ -0,0 +1,54 @@ +ast = $ast; + $this->schema = $schema; + $this->errors = []; + } + + public function reportError(Error $error) + { + $this->errors[] = $error; + } + + /** + * @return Error[] + */ + public function getErrors() + { + return $this->errors; + } + + /** + * @return DocumentNode + */ + public function getDocument() + { + return $this->ast; + } + + public function getSchema() : ?Schema + { + return $this->schema; + } +} diff --git a/src/Validator/DocumentValidator.php b/src/Validator/DocumentValidator.php index 0364839e2..98a5871c8 100644 --- a/src/Validator/DocumentValidator.php +++ b/src/Validator/DocumentValidator.php @@ -28,12 +28,13 @@ use GraphQL\Validator\Rules\NoUnusedVariables; use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged; use GraphQL\Validator\Rules\PossibleFragmentSpreads; -use GraphQL\Validator\Rules\ProvidedNonNullArguments; +use GraphQL\Validator\Rules\ProvidedRequiredArguments; use GraphQL\Validator\Rules\ProvidedRequiredArgumentsOnDirectives; use GraphQL\Validator\Rules\QueryComplexity; use GraphQL\Validator\Rules\QueryDepth; use GraphQL\Validator\Rules\QuerySecurityRule; use GraphQL\Validator\Rules\ScalarLeafs; +use GraphQL\Validator\Rules\SingleFieldSubscription; use GraphQL\Validator\Rules\UniqueArgumentNames; use GraphQL\Validator\Rules\UniqueDirectivesPerLocation; use GraphQL\Validator\Rules\UniqueFragmentNames; @@ -43,14 +44,11 @@ use GraphQL\Validator\Rules\ValidationRule; use GraphQL\Validator\Rules\ValuesOfCorrectType; use GraphQL\Validator\Rules\VariablesAreInputTypes; -use GraphQL\Validator\Rules\VariablesDefaultValueAllowed; use GraphQL\Validator\Rules\VariablesInAllowedPosition; use Throwable; use function array_filter; -use function array_map; use function array_merge; use function count; -use function implode; use function is_array; use function sprintf; @@ -113,7 +111,7 @@ public static function validate( return []; } - $typeInfo = $typeInfo ?: new TypeInfo($schema); + $typeInfo = $typeInfo ?? new TypeInfo($schema); return static::visitUsingRules($schema, $typeInfo, $ast, $rules); } @@ -142,6 +140,7 @@ public static function defaultRules() ExecutableDefinitions::class => new ExecutableDefinitions(), UniqueOperationNames::class => new UniqueOperationNames(), LoneAnonymousOperation::class => new LoneAnonymousOperation(), + SingleFieldSubscription::class => new SingleFieldSubscription(), KnownTypeNames::class => new KnownTypeNames(), FragmentsOnCompositeTypes::class => new FragmentsOnCompositeTypes(), VariablesAreInputTypes::class => new VariablesAreInputTypes(), @@ -160,8 +159,7 @@ public static function defaultRules() KnownArgumentNames::class => new KnownArgumentNames(), UniqueArgumentNames::class => new UniqueArgumentNames(), ValuesOfCorrectType::class => new ValuesOfCorrectType(), - ProvidedNonNullArguments::class => new ProvidedNonNullArguments(), - VariablesDefaultValueAllowed::class => new VariablesDefaultValueAllowed(), + ProvidedRequiredArguments::class => new ProvidedRequiredArguments(), VariablesInAllowedPosition::class => new VariablesInAllowedPosition(), OverlappingFieldsCanBeMerged::class => new OverlappingFieldsCanBeMerged(), UniqueInputFieldNames::class => new UniqueInputFieldNames(), @@ -268,11 +266,11 @@ public static function isError($value) return is_array($value) ? count(array_filter( $value, - static function ($item) { - return $item instanceof Exception || $item instanceof Throwable; + static function ($item) : bool { + return $item instanceof Throwable; } )) === count($value) - : ($value instanceof Exception || $value instanceof Throwable); + : $value instanceof Throwable; } public static function append(&$arr, $items) @@ -309,18 +307,55 @@ public static function isValidLiteralValue(Type $type, $valueNode) return $context->getErrors(); } + /** + * @param ValidationRule[]|null $rules + * + * @return Error[] + * + * @throws Exception + */ + public static function validateSDL( + DocumentNode $documentAST, + ?Schema $schemaToExtend = null, + ?array $rules = null + ) { + $usedRules = $rules ?? self::sdlRules(); + $context = new SDLValidationContext($documentAST, $schemaToExtend); + $visitors = []; + foreach ($usedRules as $rule) { + $visitors[] = $rule->getSDLVisitor($context); + } + Visitor::visit($documentAST, Visitor::visitInParallel($visitors)); + + return $context->getErrors(); + } + + public static function assertValidSDL(DocumentNode $documentAST) + { + $errors = self::validateSDL($documentAST); + if (count($errors) > 0) { + throw new Error(self::combineErrorMessages($errors)); + } + } + public static function assertValidSDLExtension(DocumentNode $documentAST, Schema $schema) { - $errors = self::visitUsingRules($schema, new TypeInfo($schema), $documentAST, self::sdlRules()); - if (count($errors) !== 0) { - throw new Error( - implode( - "\n\n", - array_map(static function (Error $error) : string { - return $error->message; - }, $errors) - ) - ); + $errors = self::validateSDL($documentAST, $schema); + if (count($errors) > 0) { + throw new Error(self::combineErrorMessages($errors)); } } + + /** + * @param Error[] $errors + */ + private static function combineErrorMessages(array $errors) : string + { + $str = ''; + foreach ($errors as $error) { + $str .= ($error->getMessage() . "\n\n"); + } + + return $str; + } } diff --git a/src/Validator/Rules/DisableIntrospection.php b/src/Validator/Rules/DisableIntrospection.php index 6e0efab18..01a93183b 100644 --- a/src/Validator/Rules/DisableIntrospection.php +++ b/src/Validator/Rules/DisableIntrospection.php @@ -31,7 +31,7 @@ public function getVisitor(ValidationContext $context) return $this->invokeIfNeeded( $context, [ - NodeKind::FIELD => static function (FieldNode $node) use ($context) { + NodeKind::FIELD => static function (FieldNode $node) use ($context) : void { if ($node->name->value !== '__type' && $node->name->value !== '__schema') { return; } diff --git a/src/Validator/Rules/ExecutableDefinitions.php b/src/Validator/Rules/ExecutableDefinitions.php index e626861dd..5966df7ef 100644 --- a/src/Validator/Rules/ExecutableDefinitions.php +++ b/src/Validator/Rules/ExecutableDefinitions.php @@ -6,11 +6,11 @@ use GraphQL\Error\Error; use GraphQL\Language\AST\DocumentNode; -use GraphQL\Language\AST\FragmentDefinitionNode; -use GraphQL\Language\AST\Node; +use GraphQL\Language\AST\ExecutableDefinitionNode; use GraphQL\Language\AST\NodeKind; -use GraphQL\Language\AST\OperationDefinitionNode; +use GraphQL\Language\AST\TypeSystemDefinitionNode; use GraphQL\Language\Visitor; +use GraphQL\Language\VisitorOperation; use GraphQL\Validator\ValidationContext; use function sprintf; @@ -25,12 +25,10 @@ class ExecutableDefinitions extends ValidationRule public function getVisitor(ValidationContext $context) { return [ - NodeKind::DOCUMENT => static function (DocumentNode $node) use ($context) { - /** @var Node $definition */ + NodeKind::DOCUMENT => static function (DocumentNode $node) use ($context) : VisitorOperation { + /** @var ExecutableDefinitionNode|TypeSystemDefinitionNode $definition */ foreach ($node->definitions as $definition) { - if ($definition instanceof OperationDefinitionNode || - $definition instanceof FragmentDefinitionNode - ) { + if ($definition instanceof ExecutableDefinitionNode) { continue; } diff --git a/src/Validator/Rules/FieldsOnCorrectType.php b/src/Validator/Rules/FieldsOnCorrectType.php index 105325536..d4d4c7288 100644 --- a/src/Validator/Rules/FieldsOnCorrectType.php +++ b/src/Validator/Rules/FieldsOnCorrectType.php @@ -16,6 +16,7 @@ use function array_keys; use function array_merge; use function arsort; +use function count; use function sprintf; class FieldsOnCorrectType extends ValidationRule @@ -23,7 +24,7 @@ class FieldsOnCorrectType extends ValidationRule public function getVisitor(ValidationContext $context) { return [ - NodeKind::FIELD => function (FieldNode $node) use ($context) { + NodeKind::FIELD => function (FieldNode $node) use ($context) : void { $type = $context->getParentType(); if (! $type) { return; @@ -156,7 +157,7 @@ public static function undefinedFieldMessage( $suggestions = Utils::quotedOrList($suggestedTypeNames); $message .= sprintf(' Did you mean to use an inline fragment on %s?', $suggestions); - } elseif (! empty($suggestedFieldNames)) { + } elseif (count($suggestedFieldNames) > 0) { $suggestions = Utils::quotedOrList($suggestedFieldNames); $message .= sprintf(' Did you mean %s?', $suggestions); diff --git a/src/Validator/Rules/FragmentsOnCompositeTypes.php b/src/Validator/Rules/FragmentsOnCompositeTypes.php index c5ca80771..db72a057b 100644 --- a/src/Validator/Rules/FragmentsOnCompositeTypes.php +++ b/src/Validator/Rules/FragmentsOnCompositeTypes.php @@ -19,7 +19,7 @@ class FragmentsOnCompositeTypes extends ValidationRule public function getVisitor(ValidationContext $context) { return [ - NodeKind::INLINE_FRAGMENT => static function (InlineFragmentNode $node) use ($context) { + NodeKind::INLINE_FRAGMENT => static function (InlineFragmentNode $node) use ($context) : void { if (! $node->typeCondition) { return; } @@ -34,7 +34,7 @@ public function getVisitor(ValidationContext $context) [$node->typeCondition] )); }, - NodeKind::FRAGMENT_DEFINITION => static function (FragmentDefinitionNode $node) use ($context) { + NodeKind::FRAGMENT_DEFINITION => static function (FragmentDefinitionNode $node) use ($context) : void { $type = TypeInfo::typeFromAST($context->getSchema(), $node->typeCondition); if (! $type || Type::isCompositeType($type)) { diff --git a/src/Validator/Rules/KnownArgumentNames.php b/src/Validator/Rules/KnownArgumentNames.php index 96fd800de..d3013797a 100644 --- a/src/Validator/Rules/KnownArgumentNames.php +++ b/src/Validator/Rules/KnownArgumentNames.php @@ -6,9 +6,11 @@ use GraphQL\Error\Error; use GraphQL\Language\AST\ArgumentNode; +use GraphQL\Language\AST\DirectiveNode; +use GraphQL\Language\AST\FieldNode; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeKind; -use GraphQL\Language\AST\NodeList; +use GraphQL\Type\Definition\Type; use GraphQL\Utils\Utils; use GraphQL\Validator\ValidationContext; use function array_map; @@ -25,58 +27,40 @@ class KnownArgumentNames extends ValidationRule { public function getVisitor(ValidationContext $context) { - return [ - NodeKind::ARGUMENT => static function (ArgumentNode $node, $key, $parent, $path, $ancestors) use ($context) { - /** @var NodeList|Node[] $ancestors */ + $knownArgumentNamesOnDirectives = new KnownArgumentNamesOnDirectives(); + + return $knownArgumentNamesOnDirectives->getVisitor($context) + [ + NodeKind::ARGUMENT => static function (ArgumentNode $node) use ($context) : void { $argDef = $context->getArgument(); if ($argDef !== null) { return; } - $argumentOf = $ancestors[count($ancestors) - 1]; - if ($argumentOf->kind === NodeKind::FIELD) { - $fieldDef = $context->getFieldDef(); - $parentType = $context->getParentType(); - if ($fieldDef && $parentType) { - $context->reportError(new Error( - self::unknownArgMessage( - $node->name->value, - $fieldDef->name, - $parentType->name, - Utils::suggestionList( - $node->name->value, - array_map( - static function ($arg) { - return $arg->name; - }, - $fieldDef->args - ) - ) - ), - [$node] - )); - } - } elseif ($argumentOf->kind === NodeKind::DIRECTIVE) { - $directive = $context->getDirective(); - if ($directive) { - $context->reportError(new Error( - self::unknownDirectiveArgMessage( - $node->name->value, - $directive->name, - Utils::suggestionList( - $node->name->value, - array_map( - static function ($arg) { - return $arg->name; - }, - $directive->args - ) - ) - ), - [$node] - )); - } + $fieldDef = $context->getFieldDef(); + $parentType = $context->getParentType(); + if ($fieldDef === null || ! ($parentType instanceof Type)) { + return; } + + $context->reportError(new Error( + self::unknownArgMessage( + $node->name->value, + $fieldDef->name, + $parentType->name, + Utils::suggestionList( + $node->name->value, + array_map( + static function ($arg) : string { + return $arg->name; + }, + $fieldDef->args + ) + ) + ), + [$node] + )); + + return; }, ]; } @@ -87,20 +71,7 @@ static function ($arg) { public static function unknownArgMessage($argName, $fieldName, $typeName, array $suggestedArgs) { $message = sprintf('Unknown argument "%s" on field "%s" of type "%s".', $argName, $fieldName, $typeName); - if (! empty($suggestedArgs)) { - $message .= sprintf(' Did you mean %s?', Utils::quotedOrList($suggestedArgs)); - } - - return $message; - } - - /** - * @param string[] $suggestedArgs - */ - public static function unknownDirectiveArgMessage($argName, $directiveName, array $suggestedArgs) - { - $message = sprintf('Unknown argument "%s" on directive "@%s".', $argName, $directiveName); - if (! empty($suggestedArgs)) { + if (isset($suggestedArgs[0])) { $message .= sprintf(' Did you mean %s?', Utils::quotedOrList($suggestedArgs)); } diff --git a/src/Validator/Rules/KnownArgumentNamesOnDirectives.php b/src/Validator/Rules/KnownArgumentNamesOnDirectives.php index a013820b7..bdde5d36d 100644 --- a/src/Validator/Rules/KnownArgumentNamesOnDirectives.php +++ b/src/Validator/Rules/KnownArgumentNamesOnDirectives.php @@ -9,13 +9,17 @@ use GraphQL\Language\AST\DirectiveNode; use GraphQL\Language\AST\InputValueDefinitionNode; use GraphQL\Language\AST\NodeKind; -use GraphQL\Language\AST\NodeList; +use GraphQL\Language\Visitor; +use GraphQL\Language\VisitorOperation; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\FieldArgument; +use GraphQL\Utils\Utils; +use GraphQL\Validator\ASTValidationContext; +use GraphQL\Validator\SDLValidationContext; use GraphQL\Validator\ValidationContext; use function array_map; use function in_array; -use function iterator_to_array; +use function sprintf; /** * Known argument names on directives @@ -25,12 +29,30 @@ */ class KnownArgumentNamesOnDirectives extends ValidationRule { - protected static function unknownDirectiveArgMessage(string $argName, string $directionName) + /** + * @param string[] $suggestedArgs + */ + public static function unknownDirectiveArgMessage($argName, $directiveName, array $suggestedArgs) { - return 'Unknown argument "' . $argName . '" on directive "@' . $directionName . '".'; + $message = sprintf('Unknown argument "%s" on directive "@%s".', $argName, $directiveName); + if (isset($suggestedArgs[0])) { + $message .= sprintf(' Did you mean %s?', Utils::quotedOrList($suggestedArgs)); + } + + return $message; + } + + public function getSDLVisitor(SDLValidationContext $context) + { + return $this->getASTVisitor($context); } public function getVisitor(ValidationContext $context) + { + return $this->getASTVisitor($context); + } + + public function getASTVisitor(ASTValidationContext $context) { $directiveArgs = []; $schema = $context->getSchema(); @@ -53,27 +75,24 @@ static function (FieldArgument $arg) : string { $name = $def->name->value; if ($def->arguments !== null) { - $arguments = $def->arguments; - - if ($arguments instanceof NodeList) { - $arguments = iterator_to_array($arguments->getIterator()); - } - - $directiveArgs[$name] = array_map(static function (InputValueDefinitionNode $arg) : string { - return $arg->name->value; - }, $arguments); + $directiveArgs[$name] = Utils::map( + $def->arguments ?? [], + static function (InputValueDefinitionNode $arg) : string { + return $arg->name->value; + } + ); } else { $directiveArgs[$name] = []; } } return [ - NodeKind::DIRECTIVE => static function (DirectiveNode $directiveNode) use ($directiveArgs, $context) { + NodeKind::DIRECTIVE => static function (DirectiveNode $directiveNode) use ($directiveArgs, $context) : VisitorOperation { $directiveName = $directiveNode->name->value; $knownArgs = $directiveArgs[$directiveName] ?? null; - if ($directiveNode->arguments === null || ! $knownArgs) { - return; + if ($directiveNode->arguments === null || $knownArgs === null) { + return Visitor::skipNode(); } foreach ($directiveNode->arguments as $argNode) { @@ -82,11 +101,14 @@ static function (FieldArgument $arg) : string { continue; } + $suggestions = Utils::suggestionList($argName, $knownArgs); $context->reportError(new Error( - self::unknownDirectiveArgMessage($argName, $directiveName), + self::unknownDirectiveArgMessage($argName, $directiveName, $suggestions), [$argNode] )); } + + return Visitor::skipNode(); }, ]; } diff --git a/src/Validator/Rules/KnownDirectives.php b/src/Validator/Rules/KnownDirectives.php index 927ce2657..758e8611a 100644 --- a/src/Validator/Rules/KnownDirectives.php +++ b/src/Validator/Rules/KnownDirectives.php @@ -4,27 +4,67 @@ namespace GraphQL\Validator\Rules; +use Exception; use GraphQL\Error\Error; use GraphQL\Language\AST\DirectiveDefinitionNode; use GraphQL\Language\AST\DirectiveNode; +use GraphQL\Language\AST\EnumTypeDefinitionNode; +use GraphQL\Language\AST\EnumTypeExtensionNode; +use GraphQL\Language\AST\EnumValueDefinitionNode; +use GraphQL\Language\AST\FieldDefinitionNode; +use GraphQL\Language\AST\FieldNode; +use GraphQL\Language\AST\FragmentDefinitionNode; +use GraphQL\Language\AST\FragmentSpreadNode; +use GraphQL\Language\AST\InlineFragmentNode; use GraphQL\Language\AST\InputObjectTypeDefinitionNode; +use GraphQL\Language\AST\InputObjectTypeExtensionNode; +use GraphQL\Language\AST\InputValueDefinitionNode; +use GraphQL\Language\AST\InterfaceTypeDefinitionNode; +use GraphQL\Language\AST\InterfaceTypeExtensionNode; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeList; +use GraphQL\Language\AST\ObjectTypeDefinitionNode; +use GraphQL\Language\AST\ObjectTypeExtensionNode; +use GraphQL\Language\AST\OperationDefinitionNode; +use GraphQL\Language\AST\ScalarTypeDefinitionNode; +use GraphQL\Language\AST\ScalarTypeExtensionNode; +use GraphQL\Language\AST\SchemaDefinitionNode; +use GraphQL\Language\AST\SchemaTypeExtensionNode; +use GraphQL\Language\AST\UnionTypeDefinitionNode; +use GraphQL\Language\AST\UnionTypeExtensionNode; +use GraphQL\Language\AST\VariableDefinitionNode; use GraphQL\Language\DirectiveLocation; +use GraphQL\Type\Definition\Directive; +use GraphQL\Utils\Utils; +use GraphQL\Validator\ASTValidationContext; +use GraphQL\Validator\SDLValidationContext; use GraphQL\Validator\ValidationContext; use function array_map; use function count; +use function get_class; use function in_array; use function sprintf; class KnownDirectives extends ValidationRule { public function getVisitor(ValidationContext $context) + { + return $this->getASTVisitor($context); + } + + public function getSDLVisitor(SDLValidationContext $context) + { + return $this->getASTVisitor($context); + } + + public function getASTVisitor(ASTValidationContext $context) { $locationsMap = []; $schema = $context->getSchema(); - $definedDirectives = $schema->getDirectives(); + $definedDirectives = $schema + ? $schema->getDirectives() + : Directive::getInternalDirectives(); foreach ($definedDirectives as $directive) { $locationsMap[$directive->name] = $directive->locations; @@ -37,11 +77,11 @@ public function getVisitor(ValidationContext $context) continue; } - $locationsMap[$def->name->value] = array_map( - static function ($name) { + $locationsMap[$def->name->value] = Utils::map( + $def->locations, + static function ($name) : string { return $name->value; - }, - $def->locations + } ); } @@ -55,7 +95,7 @@ static function ($name) { ) use ( $context, $locationsMap - ) { + ) : void { $name = $node->name->value; $locations = $locationsMap[$name] ?? null; @@ -96,8 +136,8 @@ public static function unknownDirectiveMessage($directiveName) private function getDirectiveLocationForASTPath(array $ancestors) { $appliedTo = $ancestors[count($ancestors) - 1]; - switch ($appliedTo->kind) { - case NodeKind::OPERATION_DEFINITION: + switch (true) { + case $appliedTo instanceof OperationDefinitionNode: switch ($appliedTo->operation) { case 'query': return DirectiveLocation::QUERY; @@ -107,46 +147,50 @@ private function getDirectiveLocationForASTPath(array $ancestors) return DirectiveLocation::SUBSCRIPTION; } break; - case NodeKind::FIELD: + case $appliedTo instanceof FieldNode: return DirectiveLocation::FIELD; - case NodeKind::FRAGMENT_SPREAD: + case $appliedTo instanceof FragmentSpreadNode: return DirectiveLocation::FRAGMENT_SPREAD; - case NodeKind::INLINE_FRAGMENT: + case $appliedTo instanceof InlineFragmentNode: return DirectiveLocation::INLINE_FRAGMENT; - case NodeKind::FRAGMENT_DEFINITION: + case $appliedTo instanceof FragmentDefinitionNode: return DirectiveLocation::FRAGMENT_DEFINITION; - case NodeKind::SCHEMA_DEFINITION: - case NodeKind::SCHEMA_EXTENSION: + case $appliedTo instanceof VariableDefinitionNode: + return DirectiveLocation::VARIABLE_DEFINITION; + case $appliedTo instanceof SchemaDefinitionNode: + case $appliedTo instanceof SchemaTypeExtensionNode: return DirectiveLocation::SCHEMA; - case NodeKind::SCALAR_TYPE_DEFINITION: - case NodeKind::SCALAR_TYPE_EXTENSION: + case $appliedTo instanceof ScalarTypeDefinitionNode: + case $appliedTo instanceof ScalarTypeExtensionNode: return DirectiveLocation::SCALAR; - case NodeKind::OBJECT_TYPE_DEFINITION: - case NodeKind::OBJECT_TYPE_EXTENSION: + case $appliedTo instanceof ObjectTypeDefinitionNode: + case $appliedTo instanceof ObjectTypeExtensionNode: return DirectiveLocation::OBJECT; - case NodeKind::FIELD_DEFINITION: + case $appliedTo instanceof FieldDefinitionNode: return DirectiveLocation::FIELD_DEFINITION; - case NodeKind::INTERFACE_TYPE_DEFINITION: - case NodeKind::INTERFACE_TYPE_EXTENSION: + case $appliedTo instanceof InterfaceTypeDefinitionNode: + case $appliedTo instanceof InterfaceTypeExtensionNode: return DirectiveLocation::IFACE; - case NodeKind::UNION_TYPE_DEFINITION: - case NodeKind::UNION_TYPE_EXTENSION: + case $appliedTo instanceof UnionTypeDefinitionNode: + case $appliedTo instanceof UnionTypeExtensionNode: return DirectiveLocation::UNION; - case NodeKind::ENUM_TYPE_DEFINITION: - case NodeKind::ENUM_TYPE_EXTENSION: + case $appliedTo instanceof EnumTypeDefinitionNode: + case $appliedTo instanceof EnumTypeExtensionNode: return DirectiveLocation::ENUM; - case NodeKind::ENUM_VALUE_DEFINITION: + case $appliedTo instanceof EnumValueDefinitionNode: return DirectiveLocation::ENUM_VALUE; - case NodeKind::INPUT_OBJECT_TYPE_DEFINITION: - case NodeKind::INPUT_OBJECT_TYPE_EXTENSION: + case $appliedTo instanceof InputObjectTypeDefinitionNode: + case $appliedTo instanceof InputObjectTypeExtensionNode: return DirectiveLocation::INPUT_OBJECT; - case NodeKind::INPUT_VALUE_DEFINITION: + case $appliedTo instanceof InputValueDefinitionNode: $parentNode = $ancestors[count($ancestors) - 3]; return $parentNode instanceof InputObjectTypeDefinitionNode ? DirectiveLocation::INPUT_FIELD_DEFINITION : DirectiveLocation::ARGUMENT_DEFINITION; } + + throw new Exception('Unknown directive location: ' . get_class($appliedTo)); } public static function misplacedDirectiveMessage($directiveName, $location) diff --git a/src/Validator/Rules/KnownFragmentNames.php b/src/Validator/Rules/KnownFragmentNames.php index e26e233e0..052686f28 100644 --- a/src/Validator/Rules/KnownFragmentNames.php +++ b/src/Validator/Rules/KnownFragmentNames.php @@ -15,7 +15,7 @@ class KnownFragmentNames extends ValidationRule public function getVisitor(ValidationContext $context) { return [ - NodeKind::FRAGMENT_SPREAD => static function (FragmentSpreadNode $node) use ($context) { + NodeKind::FRAGMENT_SPREAD => static function (FragmentSpreadNode $node) use ($context) : void { $fragmentName = $node->name->value; $fragment = $context->getFragment($fragmentName); if ($fragment) { diff --git a/src/Validator/Rules/KnownTypeNames.php b/src/Validator/Rules/KnownTypeNames.php index 9abaf0aeb..b852f523b 100644 --- a/src/Validator/Rules/KnownTypeNames.php +++ b/src/Validator/Rules/KnownTypeNames.php @@ -8,9 +8,11 @@ use GraphQL\Language\AST\NamedTypeNode; use GraphQL\Language\AST\NodeKind; use GraphQL\Language\Visitor; +use GraphQL\Language\VisitorOperation; use GraphQL\Utils\Utils; use GraphQL\Validator\ValidationContext; use function array_keys; +use function count; use function sprintf; /** @@ -23,7 +25,7 @@ class KnownTypeNames extends ValidationRule { public function getVisitor(ValidationContext $context) { - $skip = static function () { + $skip = static function () : VisitorOperation { return Visitor::skipNode(); }; @@ -35,7 +37,7 @@ public function getVisitor(ValidationContext $context) NodeKind::INTERFACE_TYPE_DEFINITION => $skip, NodeKind::UNION_TYPE_DEFINITION => $skip, NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $skip, - NodeKind::NAMED_TYPE => static function (NamedTypeNode $node) use ($context) { + NodeKind::NAMED_TYPE => static function (NamedTypeNode $node) use ($context) : void { $schema = $context->getSchema(); $typeName = $node->name->value; $type = $schema->getType($typeName); @@ -61,7 +63,7 @@ public function getVisitor(ValidationContext $context) public static function unknownTypeMessage($type, array $suggestedTypes) { $message = sprintf('Unknown type "%s".', $type); - if (! empty($suggestedTypes)) { + if (count($suggestedTypes) > 0) { $suggestions = Utils::quotedOrList($suggestedTypes); $message .= sprintf(' Did you mean %s?', $suggestions); diff --git a/src/Validator/Rules/LoneAnonymousOperation.php b/src/Validator/Rules/LoneAnonymousOperation.php index 40ff8218d..facc895a3 100644 --- a/src/Validator/Rules/LoneAnonymousOperation.php +++ b/src/Validator/Rules/LoneAnonymousOperation.php @@ -26,11 +26,11 @@ public function getVisitor(ValidationContext $context) $operationCount = 0; return [ - NodeKind::DOCUMENT => static function (DocumentNode $node) use (&$operationCount) { + NodeKind::DOCUMENT => static function (DocumentNode $node) use (&$operationCount) : void { $tmp = Utils::filter( $node->definitions, - static function (Node $definition) { - return $definition->kind === NodeKind::OPERATION_DEFINITION; + static function (Node $definition) : bool { + return $definition instanceof OperationDefinitionNode; } ); @@ -39,8 +39,8 @@ static function (Node $definition) { NodeKind::OPERATION_DEFINITION => static function (OperationDefinitionNode $node) use ( &$operationCount, $context - ) { - if ($node->name || $operationCount <= 1) { + ) : void { + if ($node->name !== null || $operationCount <= 1) { return; } diff --git a/src/Validator/Rules/LoneSchemaDefinition.php b/src/Validator/Rules/LoneSchemaDefinition.php index 1a8da6711..4ece976b6 100644 --- a/src/Validator/Rules/LoneSchemaDefinition.php +++ b/src/Validator/Rules/LoneSchemaDefinition.php @@ -7,7 +7,7 @@ use GraphQL\Error\Error; use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\SchemaDefinitionNode; -use GraphQL\Validator\ValidationContext; +use GraphQL\Validator\SDLValidationContext; /** * Lone Schema definition @@ -16,28 +16,40 @@ */ class LoneSchemaDefinition extends ValidationRule { - public function getVisitor(ValidationContext $context) + public static function schemaDefinitionNotAloneMessage() + { + return 'Must provide only one schema definition.'; + } + + public static function canNotDefineSchemaWithinExtensionMessage() + { + return 'Cannot define a new schema within a schema extension.'; + } + + public function getSDLVisitor(SDLValidationContext $context) { $oldSchema = $context->getSchema(); - $alreadyDefined = $oldSchema !== null ? ( - $oldSchema->getAstNode() || - $oldSchema->getQueryType() || - $oldSchema->getMutationType() || - $oldSchema->getSubscriptionType() - ) : false; + $alreadyDefined = $oldSchema !== null + ? ( + $oldSchema->getAstNode() !== null || + $oldSchema->getQueryType() !== null || + $oldSchema->getMutationType() !== null || + $oldSchema->getSubscriptionType() !== null + ) + : false; $schemaDefinitionsCount = 0; return [ - NodeKind::SCHEMA_DEFINITION => static function (SchemaDefinitionNode $node) use ($alreadyDefined, $context, &$schemaDefinitionsCount) { + NodeKind::SCHEMA_DEFINITION => static function (SchemaDefinitionNode $node) use ($alreadyDefined, $context, &$schemaDefinitionsCount) : void { if ($alreadyDefined !== false) { - $context->reportError(new Error('Cannot define a new schema within a schema extension.', $node)); + $context->reportError(new Error(self::canNotDefineSchemaWithinExtensionMessage(), $node)); return; } if ($schemaDefinitionsCount > 0) { - $context->reportError(new Error('Must provide only one schema definition.', $node)); + $context->reportError(new Error(self::schemaDefinitionNotAloneMessage(), $node)); } ++$schemaDefinitionsCount; diff --git a/src/Validator/Rules/NoFragmentCycles.php b/src/Validator/Rules/NoFragmentCycles.php index 1180a4ee9..eec546dea 100644 --- a/src/Validator/Rules/NoFragmentCycles.php +++ b/src/Validator/Rules/NoFragmentCycles.php @@ -9,14 +9,13 @@ use GraphQL\Language\AST\FragmentSpreadNode; use GraphQL\Language\AST\NodeKind; use GraphQL\Language\Visitor; +use GraphQL\Language\VisitorOperation; use GraphQL\Utils\Utils; use GraphQL\Validator\ValidationContext; -use function array_merge; use function array_pop; use function array_slice; use function count; use function implode; -use function is_array; use function sprintf; class NoFragmentCycles extends ValidationRule @@ -43,13 +42,11 @@ public function getVisitor(ValidationContext $context) $this->spreadPathIndexByName = []; return [ - NodeKind::OPERATION_DEFINITION => static function () { + NodeKind::OPERATION_DEFINITION => static function () : VisitorOperation { return Visitor::skipNode(); }, - NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) { - if (! isset($this->visitedFrags[$node->name->value])) { - $this->detectCycleRecursive($node, $context); - } + NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) : VisitorOperation { + $this->detectCycleRecursive($node, $context); return Visitor::skipNode(); }, @@ -58,12 +55,16 @@ public function getVisitor(ValidationContext $context) private function detectCycleRecursive(FragmentDefinitionNode $fragment, ValidationContext $context) { + if (isset($this->visitedFrags[$fragment->name->value])) { + return; + } + $fragmentName = $fragment->name->value; $this->visitedFrags[$fragmentName] = true; $spreadNodes = $context->getFragmentSpreads($fragment); - if (empty($spreadNodes)) { + if (count($spreadNodes) === 0) { return; } @@ -74,38 +75,24 @@ private function detectCycleRecursive(FragmentDefinitionNode $fragment, Validati $spreadName = $spreadNode->name->value; $cycleIndex = $this->spreadPathIndexByName[$spreadName] ?? null; + $this->spreadPath[] = $spreadNode; if ($cycleIndex === null) { - $this->spreadPath[] = $spreadNode; - if (empty($this->visitedFrags[$spreadName])) { - $spreadFragment = $context->getFragment($spreadName); - if ($spreadFragment) { - $this->detectCycleRecursive($spreadFragment, $context); - } + $spreadFragment = $context->getFragment($spreadName); + if ($spreadFragment) { + $this->detectCycleRecursive($spreadFragment, $context); } - array_pop($this->spreadPath); } else { - $cyclePath = array_slice($this->spreadPath, $cycleIndex); - $nodes = $cyclePath; - - if (is_array($spreadNode)) { - $nodes = array_merge($nodes, $spreadNode); - } else { - $nodes[] = $spreadNode; - } + $cyclePath = array_slice($this->spreadPath, $cycleIndex); + $fragmentNames = Utils::map(array_slice($cyclePath, 0, -1), static function ($s) { + return $s->name->value; + }); $context->reportError(new Error( - self::cycleErrorMessage( - $spreadName, - Utils::map( - $cyclePath, - static function ($s) { - return $s->name->value; - } - ) - ), - $nodes + self::cycleErrorMessage($spreadName, $fragmentNames), + $cyclePath )); } + array_pop($this->spreadPath); } $this->spreadPathIndexByName[$fragmentName] = null; @@ -119,7 +106,7 @@ public static function cycleErrorMessage($fragName, array $spreadNames = []) return sprintf( 'Cannot spread fragment "%s" within itself%s.', $fragName, - ! empty($spreadNames) ? ' via ' . implode(', ', $spreadNames) : '' + count($spreadNames) > 0 ? ' via ' . implode(', ', $spreadNames) : '' ); } } diff --git a/src/Validator/Rules/NoUndefinedVariables.php b/src/Validator/Rules/NoUndefinedVariables.php index c0cd22c75..078990c71 100644 --- a/src/Validator/Rules/NoUndefinedVariables.php +++ b/src/Validator/Rules/NoUndefinedVariables.php @@ -23,31 +23,33 @@ public function getVisitor(ValidationContext $context) return [ NodeKind::OPERATION_DEFINITION => [ - 'enter' => static function () use (&$variableNameDefined) { + 'enter' => static function () use (&$variableNameDefined) : void { $variableNameDefined = []; }, - 'leave' => static function (OperationDefinitionNode $operation) use (&$variableNameDefined, $context) { + 'leave' => static function (OperationDefinitionNode $operation) use (&$variableNameDefined, $context) : void { $usages = $context->getRecursiveVariableUsages($operation); foreach ($usages as $usage) { $node = $usage['node']; $varName = $node->name->value; - if (! empty($variableNameDefined[$varName])) { + if ($variableNameDefined[$varName] ?? false) { continue; } $context->reportError(new Error( self::undefinedVarMessage( $varName, - $operation->name ? $operation->name->value : null + $operation->name !== null + ? $operation->name->value + : null ), [$node, $operation] )); } }, ], - NodeKind::VARIABLE_DEFINITION => static function (VariableDefinitionNode $def) use (&$variableNameDefined) { + NodeKind::VARIABLE_DEFINITION => static function (VariableDefinitionNode $def) use (&$variableNameDefined) : void { $variableNameDefined[$def->variable->name->value] = true; }, ]; diff --git a/src/Validator/Rules/NoUnusedFragments.php b/src/Validator/Rules/NoUnusedFragments.php index d1cd3668f..4315c5471 100644 --- a/src/Validator/Rules/NoUnusedFragments.php +++ b/src/Validator/Rules/NoUnusedFragments.php @@ -9,6 +9,7 @@ use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\Visitor; +use GraphQL\Language\VisitorOperation; use GraphQL\Validator\ValidationContext; use function sprintf; @@ -26,18 +27,18 @@ public function getVisitor(ValidationContext $context) $this->fragmentDefs = []; return [ - NodeKind::OPERATION_DEFINITION => function ($node) { + NodeKind::OPERATION_DEFINITION => function ($node) : VisitorOperation { $this->operationDefs[] = $node; return Visitor::skipNode(); }, - NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $def) { + NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $def) : VisitorOperation { $this->fragmentDefs[] = $def; return Visitor::skipNode(); }, NodeKind::DOCUMENT => [ - 'leave' => function () use ($context) { + 'leave' => function () use ($context) : void { $fragmentNameUsed = []; foreach ($this->operationDefs as $operation) { @@ -48,7 +49,7 @@ public function getVisitor(ValidationContext $context) foreach ($this->fragmentDefs as $fragmentDef) { $fragName = $fragmentDef->name->value; - if (! empty($fragmentNameUsed[$fragName])) { + if ($fragmentNameUsed[$fragName] ?? false) { continue; } diff --git a/src/Validator/Rules/NoUnusedVariables.php b/src/Validator/Rules/NoUnusedVariables.php index e8f7ff353..343a969ba 100644 --- a/src/Validator/Rules/NoUnusedVariables.php +++ b/src/Validator/Rules/NoUnusedVariables.php @@ -22,13 +22,15 @@ public function getVisitor(ValidationContext $context) return [ NodeKind::OPERATION_DEFINITION => [ - 'enter' => function () { + 'enter' => function () : void { $this->variableDefs = []; }, - 'leave' => function (OperationDefinitionNode $operation) use ($context) { + 'leave' => function (OperationDefinitionNode $operation) use ($context) : void { $variableNameUsed = []; $usages = $context->getRecursiveVariableUsages($operation); - $opName = $operation->name ? $operation->name->value : null; + $opName = $operation->name !== null + ? $operation->name->value + : null; foreach ($usages as $usage) { $node = $usage['node']; @@ -38,7 +40,7 @@ public function getVisitor(ValidationContext $context) foreach ($this->variableDefs as $variableDef) { $variableName = $variableDef->variable->name->value; - if (! empty($variableNameUsed[$variableName])) { + if ($variableNameUsed[$variableName] ?? false) { continue; } @@ -49,7 +51,7 @@ public function getVisitor(ValidationContext $context) } }, ], - NodeKind::VARIABLE_DEFINITION => function ($def) { + NodeKind::VARIABLE_DEFINITION => function ($def) : void { $this->variableDefs[] = $def; }, ]; diff --git a/src/Validator/Rules/OverlappingFieldsCanBeMerged.php b/src/Validator/Rules/OverlappingFieldsCanBeMerged.php index 8dfa7f193..1817d0144 100644 --- a/src/Validator/Rules/OverlappingFieldsCanBeMerged.php +++ b/src/Validator/Rules/OverlappingFieldsCanBeMerged.php @@ -19,7 +19,6 @@ use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; -use GraphQL\Type\Definition\OutputType; use GraphQL\Type\Definition\Type; use GraphQL\Utils\PairSet; use GraphQL\Utils\TypeInfo; @@ -60,7 +59,7 @@ public function getVisitor(ValidationContext $context) $this->cachedFieldsAndFragmentNames = new SplObjectStorage(); return [ - NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context) { + NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context) : void { $conflicts = $this->findConflictsWithinSelectionSet( $context, $context->getParentType(), @@ -381,7 +380,7 @@ private function findConflict( ]; } - if (! $this->sameArguments($ast1->arguments ?: [], $ast2->arguments ?: [])) { + if (! $this->sameArguments($ast1->arguments ?? [], $ast2->arguments ?? [])) { return [ [$responseName, 'they have differing arguments'], [$ast1], @@ -467,30 +466,28 @@ private function sameValue(Node $value1, Node $value2) * Two types conflict if both types could not apply to a value simultaneously. * Composite types are ignored as their individual field types will be compared * later recursively. However List and Non-Null types must match. - * - * @return bool */ - private function doTypesConflict(OutputType $type1, OutputType $type2) + private function doTypesConflict(Type $type1, Type $type2) : bool { if ($type1 instanceof ListOfType) { - return $type2 instanceof ListOfType ? - $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : - true; + return $type2 instanceof ListOfType + ? $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) + : true; } if ($type2 instanceof ListOfType) { - return $type1 instanceof ListOfType ? - $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : - true; + return $type1 instanceof ListOfType + ? $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) + : true; } if ($type1 instanceof NonNull) { - return $type2 instanceof NonNull ? - $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : - true; + return $type2 instanceof NonNull + ? $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) + : true; } if ($type2 instanceof NonNull) { - return $type1 instanceof NonNull ? - $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) : - true; + return $type1 instanceof NonNull + ? $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType()) + : true; } if (Type::isLeafType($type1) || Type::isLeafType($type2)) { return $type1 !== $type2; @@ -848,14 +845,14 @@ static function ($conflict) { ], array_reduce( $conflicts, - static function ($allFields, $conflict) { + static function ($allFields, $conflict) : array { return array_merge($allFields, $conflict[1]); }, [$ast1] ), array_reduce( $conflicts, - static function ($allFields, $conflict) { + static function ($allFields, $conflict) : array { return array_merge($allFields, $conflict[2]); }, [$ast2] @@ -882,7 +879,7 @@ public static function reasonMessage($reason) { if (is_array($reason)) { $tmp = array_map( - static function ($tmp) { + static function ($tmp) : string { [$responseName, $subReason] = $tmp; $reasonMessage = self::reasonMessage($subReason); diff --git a/src/Validator/Rules/PossibleFragmentSpreads.php b/src/Validator/Rules/PossibleFragmentSpreads.php index 26611e6da..4251400ba 100644 --- a/src/Validator/Rules/PossibleFragmentSpreads.php +++ b/src/Validator/Rules/PossibleFragmentSpreads.php @@ -23,7 +23,7 @@ class PossibleFragmentSpreads extends ValidationRule public function getVisitor(ValidationContext $context) { return [ - NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) use ($context) { + NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) use ($context) : void { $fragType = $context->getType(); $parentType = $context->getParentType(); @@ -38,7 +38,7 @@ public function getVisitor(ValidationContext $context) [$node] )); }, - NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) use ($context) { + NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) use ($context) : void { $fragName = $node->name->value; $fragType = $this->getFragmentType($context, $fragName); $parentType = $context->getParentType(); @@ -68,12 +68,12 @@ private function doTypesOverlap(Schema $schema, CompositeType $fragType, Composi // Parent type is interface or union, fragment type is object type if ($parentType instanceof AbstractType && $fragType instanceof ObjectType) { - return $schema->isPossibleType($parentType, $fragType); + return $schema->isSubType($parentType, $fragType); } // Parent type is object type, fragment type is interface (or rather rare - union) if ($parentType instanceof ObjectType && $fragType instanceof AbstractType) { - return $schema->isPossibleType($fragType, $parentType); + return $schema->isSubType($fragType, $parentType); } // Both are object types: diff --git a/src/Validator/Rules/ProvidedNonNullArguments.php b/src/Validator/Rules/ProvidedNonNullArguments.php deleted file mode 100644 index 976f38b61..000000000 --- a/src/Validator/Rules/ProvidedNonNullArguments.php +++ /dev/null @@ -1,98 +0,0 @@ - [ - 'leave' => static function (FieldNode $fieldNode) use ($context) { - $fieldDef = $context->getFieldDef(); - - if (! $fieldDef) { - return Visitor::skipNode(); - } - $argNodes = $fieldNode->arguments ?: []; - - $argNodeMap = []; - foreach ($argNodes as $argNode) { - $argNodeMap[$argNode->name->value] = $argNodes; - } - foreach ($fieldDef->args as $argDef) { - $argNode = $argNodeMap[$argDef->name] ?? null; - if ($argNode || ! ($argDef->getType() instanceof NonNull)) { - continue; - } - - $context->reportError(new Error( - self::missingFieldArgMessage($fieldNode->name->value, $argDef->name, $argDef->getType()), - [$fieldNode] - )); - } - }, - ], - NodeKind::DIRECTIVE => [ - 'leave' => static function (DirectiveNode $directiveNode) use ($context) { - $directiveDef = $context->getDirective(); - if (! $directiveDef) { - return Visitor::skipNode(); - } - $argNodes = $directiveNode->arguments ?: []; - $argNodeMap = []; - foreach ($argNodes as $argNode) { - $argNodeMap[$argNode->name->value] = $argNodes; - } - - foreach ($directiveDef->args as $argDef) { - $argNode = $argNodeMap[$argDef->name] ?? null; - if ($argNode || ! ($argDef->getType() instanceof NonNull)) { - continue; - } - - $context->reportError(new Error( - self::missingDirectiveArgMessage( - $directiveNode->name->value, - $argDef->name, - $argDef->getType() - ), - [$directiveNode] - )); - } - }, - ], - ]; - } - - public static function missingFieldArgMessage($fieldName, $argName, $type) - { - return sprintf( - 'Field "%s" argument "%s" of type "%s" is required but not provided.', - $fieldName, - $argName, - $type - ); - } - - public static function missingDirectiveArgMessage($directiveName, $argName, $type) - { - return sprintf( - 'Directive "@%s" argument "%s" of type "%s" is required but not provided.', - $directiveName, - $argName, - $type - ); - } -} diff --git a/src/Validator/Rules/ProvidedRequiredArguments.php b/src/Validator/Rules/ProvidedRequiredArguments.php new file mode 100644 index 000000000..77a4aa5a8 --- /dev/null +++ b/src/Validator/Rules/ProvidedRequiredArguments.php @@ -0,0 +1,62 @@ +getVisitor($context) + [ + NodeKind::FIELD => [ + 'leave' => static function (FieldNode $fieldNode) use ($context) : ?VisitorOperation { + $fieldDef = $context->getFieldDef(); + + if (! $fieldDef) { + return Visitor::skipNode(); + } + $argNodes = $fieldNode->arguments ?? []; + + $argNodeMap = []; + foreach ($argNodes as $argNode) { + $argNodeMap[$argNode->name->value] = $argNode; + } + foreach ($fieldDef->args as $argDef) { + $argNode = $argNodeMap[$argDef->name] ?? null; + if ($argNode || ! $argDef->isRequired()) { + continue; + } + + $context->reportError(new Error( + self::missingFieldArgMessage($fieldNode->name->value, $argDef->name, $argDef->getType()), + [$fieldNode] + )); + } + + return null; + }, + ], + ]; + } + + public static function missingFieldArgMessage($fieldName, $argName, $type) + { + return sprintf( + 'Field "%s" argument "%s" of type "%s" is required but not provided.', + $fieldName, + $argName, + $type + ); + } +} diff --git a/src/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php b/src/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php index 875071147..f8b34c713 100644 --- a/src/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php +++ b/src/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php @@ -8,18 +8,17 @@ use GraphQL\Language\AST\ArgumentNode; use GraphQL\Language\AST\DirectiveDefinitionNode; use GraphQL\Language\AST\DirectiveNode; -use GraphQL\Language\AST\NamedTypeNode; -use GraphQL\Language\AST\Node; +use GraphQL\Language\AST\InputValueDefinitionNode; use GraphQL\Language\AST\NodeKind; -use GraphQL\Language\AST\NodeList; use GraphQL\Language\AST\NonNullTypeNode; +use GraphQL\Language\Printer; +use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\FieldArgument; -use GraphQL\Type\Definition\NonNull; use GraphQL\Utils\Utils; +use GraphQL\Validator\ASTValidationContext; +use GraphQL\Validator\SDLValidationContext; use GraphQL\Validator\ValidationContext; use function array_filter; -use function is_array; -use function iterator_to_array; /** * Provided required arguments on directives @@ -29,21 +28,34 @@ */ class ProvidedRequiredArgumentsOnDirectives extends ValidationRule { - protected static function missingDirectiveArgMessage(string $directiveName, string $argName) + public static function missingDirectiveArgMessage(string $directiveName, string $argName, string $type) { - return 'Directive "' . $directiveName . '" argument "' . $argName . '" is required but ont provided.'; + return 'Directive "@' . $directiveName . '" argument "' . $argName + . '" of type "' . $type . '" is required but not provided.'; + } + + public function getSDLVisitor(SDLValidationContext $context) + { + return $this->getASTVisitor($context); } public function getVisitor(ValidationContext $context) + { + return $this->getASTVisitor($context); + } + + public function getASTVisitor(ASTValidationContext $context) { $requiredArgsMap = []; $schema = $context->getSchema(); - $definedDirectives = $schema->getDirectives(); + $definedDirectives = $schema + ? $schema->getDirectives() + : Directive::getInternalDirectives(); foreach ($definedDirectives as $directive) { $requiredArgsMap[$directive->name] = Utils::keyMap( array_filter($directive->args, static function (FieldArgument $arg) : bool { - return $arg->getType() instanceof NonNull && ! isset($arg->defaultValue); + return $arg->isRequired(); }), static function (FieldArgument $arg) : string { return $arg->name; @@ -56,55 +68,61 @@ static function (FieldArgument $arg) : string { if (! ($def instanceof DirectiveDefinitionNode)) { continue; } - - if (is_array($def->arguments)) { - $arguments = $def->arguments; - } elseif ($def->arguments instanceof NodeList) { - $arguments = iterator_to_array($def->arguments->getIterator()); - } else { - $arguments = null; - } + $arguments = $def->arguments ?? []; $requiredArgsMap[$def->name->value] = Utils::keyMap( - $arguments ? array_filter($arguments, static function (Node $argument) : bool { - return $argument instanceof NonNullTypeNode && + Utils::filter($arguments, static function (InputValueDefinitionNode $argument) : bool { + return $argument->type instanceof NonNullTypeNode && ( ! isset($argument->defaultValue) || $argument->defaultValue === null ); - }) : [], - static function (NamedTypeNode $argument) : string { + }), + static function (InputValueDefinitionNode $argument) : string { return $argument->name->value; } ); } return [ - NodeKind::DIRECTIVE => static function (DirectiveNode $directiveNode) use ($requiredArgsMap, $context) { - $directiveName = $directiveNode->name->value; - $requiredArgs = $requiredArgsMap[$directiveName] ?? null; - if (! $requiredArgs) { - return; - } - - $argNodes = $directiveNode->arguments ?: []; - $argNodeMap = Utils::keyMap( - $argNodes, - static function (ArgumentNode $arg) : string { - return $arg->name->value; + NodeKind::DIRECTIVE => [ + // Validate on leave to allow for deeper errors to appear first. + 'leave' => static function (DirectiveNode $directiveNode) use ($requiredArgsMap, $context) : ?string { + $directiveName = $directiveNode->name->value; + $requiredArgs = $requiredArgsMap[$directiveName] ?? null; + if (! $requiredArgs) { + return null; } - ); - foreach ($requiredArgs as $argName => $arg) { - if (isset($argNodeMap[$argName])) { - continue; + $argNodes = $directiveNode->arguments ?? []; + $argNodeMap = Utils::keyMap( + $argNodes, + static function (ArgumentNode $arg) : string { + return $arg->name->value; + } + ); + + foreach ($requiredArgs as $argName => $arg) { + if (isset($argNodeMap[$argName])) { + continue; + } + + if ($arg instanceof FieldArgument) { + $argType = (string) $arg->getType(); + } elseif ($arg instanceof InputValueDefinitionNode) { + $argType = Printer::doPrint($arg->type); + } else { + $argType = ''; + } + + $context->reportError( + new Error(static::missingDirectiveArgMessage($directiveName, $argName, $argType), [$directiveNode]) + ); } - $context->reportError( - new Error(static::missingDirectiveArgMessage($directiveName, $argName), [$directiveNode]) - ); - } - }, + return null; + }, + ], ]; } } diff --git a/src/Validator/Rules/QueryComplexity.php b/src/Validator/Rules/QueryComplexity.php index 85bd1ac19..1a1eb4bd8 100644 --- a/src/Validator/Rules/QueryComplexity.php +++ b/src/Validator/Rules/QueryComplexity.php @@ -15,11 +15,12 @@ use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Language\Visitor; +use GraphQL\Language\VisitorOperation; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Validator\ValidationContext; use function array_map; -use function call_user_func_array; +use function count; use function implode; use function method_exists; use function sprintf; @@ -41,6 +42,9 @@ class QueryComplexity extends QuerySecurityRule /** @var ValidationContext */ private $context; + /** @var int */ + private $complexity; + public function __construct($maxQueryComplexity) { $this->setMaxQueryComplexity($maxQueryComplexity); @@ -52,12 +56,12 @@ public function getVisitor(ValidationContext $context) $this->variableDefs = new ArrayObject(); $this->fieldNodeAndDefs = new ArrayObject(); - $complexity = 0; + $this->complexity = 0; return $this->invokeIfNeeded( $context, [ - NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context) { + NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context) : void { $this->fieldNodeAndDefs = $this->collectFieldASTsAndDefs( $context, $context->getParentType(), @@ -66,29 +70,29 @@ public function getVisitor(ValidationContext $context) $this->fieldNodeAndDefs ); }, - NodeKind::VARIABLE_DEFINITION => function ($def) { + NodeKind::VARIABLE_DEFINITION => function ($def) : VisitorOperation { $this->variableDefs[] = $def; return Visitor::skipNode(); }, NodeKind::OPERATION_DEFINITION => [ - 'leave' => function (OperationDefinitionNode $operationDefinition) use ($context, &$complexity) { + 'leave' => function (OperationDefinitionNode $operationDefinition) use ($context, &$complexity) : void { $errors = $context->getErrors(); - if (! empty($errors)) { + if (count($errors) > 0) { return; } - $complexity = $this->fieldComplexity($operationDefinition, $complexity); + $this->complexity = $this->fieldComplexity($operationDefinition, $complexity); - if ($complexity <= $this->getMaxQueryComplexity()) { + if ($this->getQueryComplexity() <= $this->getMaxQueryComplexity()) { return; } $context->reportError( new Error(self::maxQueryComplexityErrorMessage( $this->getMaxQueryComplexity(), - $complexity + $this->getQueryComplexity() )) ); }, @@ -110,9 +114,8 @@ private function fieldComplexity($node, $complexity = 0) private function nodeComplexity(Node $node, $complexity = 0) { - switch ($node->kind) { - case NodeKind::FIELD: - /** @var FieldNode $node */ + switch (true) { + case $node instanceof FieldNode: // default values $args = []; $complexityFn = FieldDefinition::DEFAULT_COMPLEXITY_FN; @@ -140,19 +143,17 @@ private function nodeComplexity(Node $node, $complexity = 0) } } - $complexity += call_user_func_array($complexityFn, [$childrenComplexity, $args]); + $complexity += $complexityFn($childrenComplexity, $args); break; - case NodeKind::INLINE_FRAGMENT: - /** @var InlineFragmentNode $node */ + case $node instanceof InlineFragmentNode: // node has children? if (isset($node->selectionSet)) { $complexity = $this->fieldComplexity($node, $complexity); } break; - case NodeKind::FRAGMENT_SPREAD: - /** @var FragmentSpreadNode $node */ + case $node instanceof FragmentSpreadNode: $fragment = $this->getFragment($node); if ($fragment !== null) { @@ -191,7 +192,7 @@ private function directiveExcludesField(FieldNode $node) $this->variableDefs, $this->getRawVariableValues() ); - if (! empty($errors)) { + if (count($errors ?? []) > 0) { throw new Error(implode( "\n\n", array_map( @@ -209,11 +210,16 @@ static function ($error) { return ! $directiveArgsIf; } - $directive = Directive::skipDirective(); - $directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues); + if ($directiveNode->name->value === Directive::SKIP_NAME) { + $directive = Directive::skipDirective(); + /** @var bool $directiveArgsIf */ + $directiveArgsIf = Values::getArgumentValues($directive, $directiveNode, $variableValues)['if']; - return $directiveArgsIf['if']; + return $directiveArgsIf; + } } + + return false; } public function getRawVariableValues() @@ -226,7 +232,7 @@ public function getRawVariableValues() */ public function setRawVariableValues(?array $rawVariableValues = null) { - $this->rawVariableValues = $rawVariableValues ?: []; + $this->rawVariableValues = $rawVariableValues ?? []; } private function buildFieldArguments(FieldNode $node) @@ -244,7 +250,7 @@ private function buildFieldArguments(FieldNode $node) $rawVariableValues ); - if (! empty($errors)) { + if (count($errors ?? []) > 0) { throw new Error(implode( "\n\n", array_map( @@ -262,6 +268,11 @@ static function ($error) { return $args; } + public function getQueryComplexity() + { + return $this->complexity; + } + public function getMaxQueryComplexity() { return $this->maxQueryComplexity; diff --git a/src/Validator/Rules/QueryDepth.php b/src/Validator/Rules/QueryDepth.php index e4b31a0f8..9b83d061e 100644 --- a/src/Validator/Rules/QueryDepth.php +++ b/src/Validator/Rules/QueryDepth.php @@ -5,6 +5,9 @@ namespace GraphQL\Validator\Rules; use GraphQL\Error\Error; +use GraphQL\Language\AST\FieldNode; +use GraphQL\Language\AST\FragmentSpreadNode; +use GraphQL\Language\AST\InlineFragmentNode; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\OperationDefinitionNode; @@ -28,7 +31,7 @@ public function getVisitor(ValidationContext $context) $context, [ NodeKind::OPERATION_DEFINITION => [ - 'leave' => function (OperationDefinitionNode $operationDefinition) use ($context) { + 'leave' => function (OperationDefinitionNode $operationDefinition) use ($context) : void { $maxDepth = $this->fieldDepth($operationDefinition); if ($maxDepth <= $this->getMaxQueryDepth()) { @@ -57,9 +60,8 @@ private function fieldDepth($node, $depth = 0, $maxDepth = 0) private function nodeDepth(Node $node, $depth = 0, $maxDepth = 0) { - switch ($node->kind) { - case NodeKind::FIELD: - /** @var FieldNode $node */ + switch (true) { + case $node instanceof FieldNode: // node has children? if ($node->selectionSet !== null) { // update maxDepth if needed @@ -70,16 +72,14 @@ private function nodeDepth(Node $node, $depth = 0, $maxDepth = 0) } break; - case NodeKind::INLINE_FRAGMENT: - /** @var InlineFragmentNode $node */ + case $node instanceof InlineFragmentNode: // node has children? if ($node->selectionSet !== null) { $maxDepth = $this->fieldDepth($node, $depth, $maxDepth); } break; - case NodeKind::FRAGMENT_SPREAD: - /** @var FragmentSpreadNode $node */ + case $node instanceof FragmentSpreadNode: $fragment = $this->getFragment($node); if ($fragment !== null) { diff --git a/src/Validator/Rules/QuerySecurityRule.php b/src/Validator/Rules/QuerySecurityRule.php index c5f02603f..f4fa5e1dd 100644 --- a/src/Validator/Rules/QuerySecurityRule.php +++ b/src/Validator/Rules/QuerySecurityRule.php @@ -9,7 +9,6 @@ use GraphQL\Language\AST\FragmentDefinitionNode; use GraphQL\Language\AST\FragmentSpreadNode; use GraphQL\Language\AST\InlineFragmentNode; -use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Type\Definition\Type; use GraphQL\Type\Introspection; @@ -110,13 +109,12 @@ protected function collectFieldASTsAndDefs( ?ArrayObject $visitedFragmentNames = null, ?ArrayObject $astAndDefs = null ) { - $_visitedFragmentNames = $visitedFragmentNames ?: new ArrayObject(); - $_astAndDefs = $astAndDefs ?: new ArrayObject(); + $_visitedFragmentNames = $visitedFragmentNames ?? new ArrayObject(); + $_astAndDefs = $astAndDefs ?? new ArrayObject(); foreach ($selectionSet->selections as $selection) { - switch ($selection->kind) { - case NodeKind::FIELD: - /** @var FieldNode $selection */ + switch (true) { + case $selection instanceof FieldNode: $fieldName = $selection->name->value; $fieldDef = null; if ($parentType && method_exists($parentType, 'getFields')) { @@ -142,8 +140,7 @@ protected function collectFieldASTsAndDefs( // create field context $_astAndDefs[$responseName][] = [$selection, $fieldDef]; break; - case NodeKind::INLINE_FRAGMENT: - /** @var InlineFragmentNode $selection */ + case $selection instanceof InlineFragmentNode: $_astAndDefs = $this->collectFieldASTsAndDefs( $context, TypeInfo::typeFromAST($context->getSchema(), $selection->typeCondition), @@ -152,11 +149,10 @@ protected function collectFieldASTsAndDefs( $_astAndDefs ); break; - case NodeKind::FRAGMENT_SPREAD: - /** @var FragmentSpreadNode $selection */ + case $selection instanceof FragmentSpreadNode: $fragName = $selection->name->value; - if (empty($_visitedFragmentNames[$fragName])) { + if (! ($_visitedFragmentNames[$fragName] ?? false)) { $_visitedFragmentNames[$fragName] = true; $fragment = $context->getFragment($fragName); diff --git a/src/Validator/Rules/ScalarLeafs.php b/src/Validator/Rules/ScalarLeafs.php index 1bd25152b..f1714239e 100644 --- a/src/Validator/Rules/ScalarLeafs.php +++ b/src/Validator/Rules/ScalarLeafs.php @@ -16,7 +16,7 @@ class ScalarLeafs extends ValidationRule public function getVisitor(ValidationContext $context) { return [ - NodeKind::FIELD => static function (FieldNode $node) use ($context) { + NodeKind::FIELD => static function (FieldNode $node) use ($context) : void { $type = $context->getType(); if (! $type) { return; diff --git a/src/Validator/Rules/SingleFieldSubscription.php b/src/Validator/Rules/SingleFieldSubscription.php new file mode 100644 index 000000000..b98ee95ed --- /dev/null +++ b/src/Validator/Rules/SingleFieldSubscription.php @@ -0,0 +1,57 @@ + + */ + public function getVisitor(ValidationContext $context) : array + { + return [ + NodeKind::OPERATION_DEFINITION => static function (OperationDefinitionNode $node) use ($context) : VisitorOperation { + if ($node->operation === 'subscription') { + $selections = $node->selectionSet->selections; + + if (count($selections) !== 1) { + if ($selections instanceof NodeList) { + $offendingSelections = $selections->splice(1, count($selections)); + } else { + $offendingSelections = array_splice($selections, 1); + } + + $context->reportError(new Error( + self::multipleFieldsInOperation($node->name->value ?? null), + $offendingSelections + )); + } + } + + return Visitor::skipNode(); + }, + ]; + } + + public static function multipleFieldsInOperation(?string $operationName) : string + { + if ($operationName === null) { + return sprintf('Anonymous Subscription must select only one top level field.'); + } + + return sprintf('Subscription "%s" must select only one top level field.', $operationName); + } +} diff --git a/src/Validator/Rules/UniqueArgumentNames.php b/src/Validator/Rules/UniqueArgumentNames.php index 2e83d4308..58daecf68 100644 --- a/src/Validator/Rules/UniqueArgumentNames.php +++ b/src/Validator/Rules/UniqueArgumentNames.php @@ -9,6 +9,9 @@ use GraphQL\Language\AST\NameNode; use GraphQL\Language\AST\NodeKind; use GraphQL\Language\Visitor; +use GraphQL\Language\VisitorOperation; +use GraphQL\Validator\ASTValidationContext; +use GraphQL\Validator\SDLValidationContext; use GraphQL\Validator\ValidationContext; use function sprintf; @@ -17,20 +20,30 @@ class UniqueArgumentNames extends ValidationRule /** @var NameNode[] */ public $knownArgNames; + public function getSDLVisitor(SDLValidationContext $context) + { + return $this->getASTVisitor($context); + } + public function getVisitor(ValidationContext $context) + { + return $this->getASTVisitor($context); + } + + public function getASTVisitor(ASTValidationContext $context) { $this->knownArgNames = []; return [ - NodeKind::FIELD => function () { + NodeKind::FIELD => function () : void { $this->knownArgNames = []; }, - NodeKind::DIRECTIVE => function () { + NodeKind::DIRECTIVE => function () : void { $this->knownArgNames = []; }, - NodeKind::ARGUMENT => function (ArgumentNode $node) use ($context) { + NodeKind::ARGUMENT => function (ArgumentNode $node) use ($context) : VisitorOperation { $argName = $node->name->value; - if (! empty($this->knownArgNames[$argName])) { + if ($this->knownArgNames[$argName] ?? false) { $context->reportError(new Error( self::duplicateArgMessage($argName), [$this->knownArgNames[$argName], $node->name] diff --git a/src/Validator/Rules/UniqueDirectivesPerLocation.php b/src/Validator/Rules/UniqueDirectivesPerLocation.php index f0e8c4591..02844af73 100644 --- a/src/Validator/Rules/UniqueDirectivesPerLocation.php +++ b/src/Validator/Rules/UniqueDirectivesPerLocation.php @@ -5,25 +5,70 @@ namespace GraphQL\Validator\Rules; use GraphQL\Error\Error; +use GraphQL\Language\AST\DirectiveDefinitionNode; use GraphQL\Language\AST\DirectiveNode; use GraphQL\Language\AST\Node; +use GraphQL\Type\Definition\Directive; +use GraphQL\Validator\ASTValidationContext; +use GraphQL\Validator\SDLValidationContext; use GraphQL\Validator\ValidationContext; use function sprintf; +/** + * Unique directive names per location + * + * A GraphQL document is only valid if all non-repeatable directives at + * a given location are uniquely named. + */ class UniqueDirectivesPerLocation extends ValidationRule { public function getVisitor(ValidationContext $context) { + return $this->getASTVisitor($context); + } + + public function getSDLVisitor(SDLValidationContext $context) + { + return $this->getASTVisitor($context); + } + + public function getASTVisitor(ASTValidationContext $context) + { + $uniqueDirectiveMap = []; + + $schema = $context->getSchema(); + $definedDirectives = $schema !== null + ? $schema->getDirectives() + : Directive::getInternalDirectives(); + foreach ($definedDirectives as $directive) { + $uniqueDirectiveMap[$directive->name] = ! $directive->isRepeatable; + } + + $astDefinitions = $context->getDocument()->definitions; + foreach ($astDefinitions as $definition) { + if (! ($definition instanceof DirectiveDefinitionNode)) { + continue; + } + + $uniqueDirectiveMap[$definition->name->value] = $definition->repeatable; + } + return [ - 'enter' => static function (Node $node) use ($context) { + 'enter' => static function (Node $node) use ($uniqueDirectiveMap, $context) : void { if (! isset($node->directives)) { return; } $knownDirectives = []; + + /** @var DirectiveNode $directive */ foreach ($node->directives as $directive) { - /** @var DirectiveNode $directive */ $directiveName = $directive->name->value; + + if (! isset($uniqueDirectiveMap[$directiveName])) { + continue; + } + if (isset($knownDirectives[$directiveName])) { $context->reportError(new Error( self::duplicateDirectiveMessage($directiveName), diff --git a/src/Validator/Rules/UniqueFragmentNames.php b/src/Validator/Rules/UniqueFragmentNames.php index 475e6b3fa..e9dba8a92 100644 --- a/src/Validator/Rules/UniqueFragmentNames.php +++ b/src/Validator/Rules/UniqueFragmentNames.php @@ -9,6 +9,7 @@ use GraphQL\Language\AST\NameNode; use GraphQL\Language\AST\NodeKind; use GraphQL\Language\Visitor; +use GraphQL\Language\VisitorOperation; use GraphQL\Validator\ValidationContext; use function sprintf; @@ -22,12 +23,12 @@ public function getVisitor(ValidationContext $context) $this->knownFragmentNames = []; return [ - NodeKind::OPERATION_DEFINITION => static function () { + NodeKind::OPERATION_DEFINITION => static function () : VisitorOperation { return Visitor::skipNode(); }, - NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) { + NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context) : VisitorOperation { $fragmentName = $node->name->value; - if (empty($this->knownFragmentNames[$fragmentName])) { + if (! isset($this->knownFragmentNames[$fragmentName])) { $this->knownFragmentNames[$fragmentName] = $node->name; } else { $context->reportError(new Error( diff --git a/src/Validator/Rules/UniqueInputFieldNames.php b/src/Validator/Rules/UniqueInputFieldNames.php index 6426b437e..541a37293 100644 --- a/src/Validator/Rules/UniqueInputFieldNames.php +++ b/src/Validator/Rules/UniqueInputFieldNames.php @@ -5,40 +5,54 @@ namespace GraphQL\Validator\Rules; use GraphQL\Error\Error; +use GraphQL\Language\AST\NameNode; use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\ObjectFieldNode; use GraphQL\Language\Visitor; +use GraphQL\Language\VisitorOperation; +use GraphQL\Validator\ASTValidationContext; +use GraphQL\Validator\SDLValidationContext; use GraphQL\Validator\ValidationContext; use function array_pop; use function sprintf; class UniqueInputFieldNames extends ValidationRule { - /** @var string[] */ + /** @var array */ public $knownNames; - /** @var string[][] */ + /** @var array> */ public $knownNameStack; public function getVisitor(ValidationContext $context) + { + return $this->getASTVisitor($context); + } + + public function getSDLVisitor(SDLValidationContext $context) + { + return $this->getASTVisitor($context); + } + + public function getASTVisitor(ASTValidationContext $context) { $this->knownNames = []; $this->knownNameStack = []; return [ NodeKind::OBJECT => [ - 'enter' => function () { + 'enter' => function () : void { $this->knownNameStack[] = $this->knownNames; $this->knownNames = []; }, - 'leave' => function () { + 'leave' => function () : void { $this->knownNames = array_pop($this->knownNameStack); }, ], - NodeKind::OBJECT_FIELD => function (ObjectFieldNode $node) use ($context) { + NodeKind::OBJECT_FIELD => function (ObjectFieldNode $node) use ($context) : VisitorOperation { $fieldName = $node->name->value; - if (! empty($this->knownNames[$fieldName])) { + if (isset($this->knownNames[$fieldName])) { $context->reportError(new Error( self::duplicateInputFieldMessage($fieldName), [$this->knownNames[$fieldName], $node->name] diff --git a/src/Validator/Rules/UniqueOperationNames.php b/src/Validator/Rules/UniqueOperationNames.php index 969b90e7f..c4a0777fb 100644 --- a/src/Validator/Rules/UniqueOperationNames.php +++ b/src/Validator/Rules/UniqueOperationNames.php @@ -9,6 +9,7 @@ use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\Visitor; +use GraphQL\Language\VisitorOperation; use GraphQL\Validator\ValidationContext; use function sprintf; @@ -22,11 +23,11 @@ public function getVisitor(ValidationContext $context) $this->knownOperationNames = []; return [ - NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) use ($context) { + NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) use ($context) : VisitorOperation { $operationName = $node->name; - if ($operationName) { - if (empty($this->knownOperationNames[$operationName->value])) { + if ($operationName !== null) { + if (! isset($this->knownOperationNames[$operationName->value])) { $this->knownOperationNames[$operationName->value] = $operationName; } else { $context->reportError(new Error( @@ -38,7 +39,7 @@ public function getVisitor(ValidationContext $context) return Visitor::skipNode(); }, - NodeKind::FRAGMENT_DEFINITION => static function () { + NodeKind::FRAGMENT_DEFINITION => static function () : VisitorOperation { return Visitor::skipNode(); }, ]; diff --git a/src/Validator/Rules/UniqueVariableNames.php b/src/Validator/Rules/UniqueVariableNames.php index f050a7342..f6db14e65 100644 --- a/src/Validator/Rules/UniqueVariableNames.php +++ b/src/Validator/Rules/UniqueVariableNames.php @@ -21,12 +21,12 @@ public function getVisitor(ValidationContext $context) $this->knownVariableNames = []; return [ - NodeKind::OPERATION_DEFINITION => function () { + NodeKind::OPERATION_DEFINITION => function () : void { $this->knownVariableNames = []; }, - NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context) { + NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context) : void { $variableName = $node->variable->name->value; - if (empty($this->knownVariableNames[$variableName])) { + if (! isset($this->knownVariableNames[$variableName])) { $this->knownVariableNames[$variableName] = $node->variable->name; } else { $context->reportError(new Error( diff --git a/src/Validator/Rules/ValidationRule.php b/src/Validator/Rules/ValidationRule.php index fe8504d16..461aafeeb 100644 --- a/src/Validator/Rules/ValidationRule.php +++ b/src/Validator/Rules/ValidationRule.php @@ -4,6 +4,7 @@ namespace GraphQL\Validator\Rules; +use GraphQL\Validator\SDLValidationContext; use GraphQL\Validator\ValidationContext; use function class_alias; @@ -14,7 +15,7 @@ abstract class ValidationRule public function getName() { - return $this->name ?: static::class; + return $this->name === '' || $this->name === null ? static::class : $this->name; } public function __invoke(ValidationContext $context) @@ -29,7 +30,22 @@ public function __invoke(ValidationContext $context) * * @return mixed[] */ - abstract public function getVisitor(ValidationContext $context); + public function getVisitor(ValidationContext $context) + { + return []; + } + + /** + * Returns structure suitable for GraphQL\Language\Visitor + * + * @see \GraphQL\Language\Visitor + * + * @return mixed[] + */ + public function getSDLVisitor(SDLValidationContext $context) + { + return []; + } } class_alias(ValidationRule::class, 'GraphQL\Validator\Rules\AbstractValidationRule'); diff --git a/src/Validator/Rules/ValuesOfCorrectType.php b/src/Validator/Rules/ValuesOfCorrectType.php index 4e8199b60..9c48c1253 100644 --- a/src/Validator/Rules/ValuesOfCorrectType.php +++ b/src/Validator/Rules/ValuesOfCorrectType.php @@ -4,7 +4,6 @@ namespace GraphQL\Validator\Rules; -use Exception; use GraphQL\Error\Error; use GraphQL\Language\AST\BooleanValueNode; use GraphQL\Language\AST\EnumValueNode; @@ -18,11 +17,12 @@ use GraphQL\Language\AST\ObjectValueNode; use GraphQL\Language\AST\StringValueNode; use GraphQL\Language\AST\ValueNode; +use GraphQL\Language\AST\VariableNode; use GraphQL\Language\Printer; use GraphQL\Language\Visitor; +use GraphQL\Language\VisitorOperation; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumValueDefinition; -use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; @@ -52,11 +52,11 @@ public function getVisitor(ValidationContext $context) return [ NodeKind::FIELD => [ - 'enter' => static function (FieldNode $node) use (&$fieldName) { + 'enter' => static function (FieldNode $node) use (&$fieldName) : void { $fieldName = $node->name->value; }, ], - NodeKind::NULL => static function (NullValueNode $node) use ($context, &$fieldName) { + NodeKind::NULL => static function (NullValueNode $node) use ($context, &$fieldName) : void { $type = $context->getInputType(); if (! ($type instanceof NonNull)) { return; @@ -69,7 +69,7 @@ public function getVisitor(ValidationContext $context) ) ); }, - NodeKind::LST => function (ListValueNode $node) use ($context, &$fieldName) { + NodeKind::LST => function (ListValueNode $node) use ($context, &$fieldName) : ?VisitorOperation { // Note: TypeInfo will traverse into a list's item type, so look to the // parent input type to check if it is a list. $type = Type::getNullableType($context->getParentInputType()); @@ -78,6 +78,8 @@ public function getVisitor(ValidationContext $context) return Visitor::skipNode(); } + + return null; }, NodeKind::OBJECT => function (ObjectValueNode $node) use ($context, &$fieldName) { // Note: TypeInfo will traverse into a list's item type, so look to the @@ -94,7 +96,7 @@ public function getVisitor(ValidationContext $context) $nodeFields = iterator_to_array($node->fields); $fieldNodeMap = array_combine( array_map( - static function ($field) { + static function ($field) : string { return $field->name->value; }, $nodeFields @@ -103,7 +105,7 @@ static function ($field) { ); foreach ($inputFields as $fieldName => $fieldDef) { $fieldType = $fieldDef->getType(); - if (isset($fieldNodeMap[$fieldName]) || ! ($fieldType instanceof NonNull)) { + if (isset($fieldNodeMap[$fieldName]) || ! $fieldDef->isRequired()) { continue; } @@ -115,9 +117,10 @@ static function ($field) { ); } }, - NodeKind::OBJECT_FIELD => static function (ObjectFieldNode $node) use ($context) { + NodeKind::OBJECT_FIELD => static function (ObjectFieldNode $node) use ($context) : void { $parentType = Type::getNamedType($context->getParentInputType()); - $fieldType = $context->getInputType(); + /** @var ScalarType|EnumType|InputObjectType|ListOfType|NonNull $fieldType */ + $fieldType = $context->getInputType(); if ($fieldType || ! ($parentType instanceof InputObjectType)) { return; } @@ -137,7 +140,7 @@ static function ($field) { ) ); }, - NodeKind::ENUM => function (EnumValueNode $node) use ($context, &$fieldName) { + NodeKind::ENUM => function (EnumValueNode $node) use ($context, &$fieldName) : void { $type = Type::getNamedType($context->getInputType()); if (! $type instanceof EnumType) { $this->isValidScalar($context, $node, $fieldName); @@ -156,16 +159,16 @@ static function ($field) { ); } }, - NodeKind::INT => function (IntValueNode $node) use ($context, &$fieldName) { + NodeKind::INT => function (IntValueNode $node) use ($context, &$fieldName) : void { $this->isValidScalar($context, $node, $fieldName); }, - NodeKind::FLOAT => function (FloatValueNode $node) use ($context, &$fieldName) { + NodeKind::FLOAT => function (FloatValueNode $node) use ($context, &$fieldName) : void { $this->isValidScalar($context, $node, $fieldName); }, - NodeKind::STRING => function (StringValueNode $node) use ($context, &$fieldName) { + NodeKind::STRING => function (StringValueNode $node) use ($context, &$fieldName) : void { $this->isValidScalar($context, $node, $fieldName); }, - NodeKind::BOOLEAN => function (BooleanValueNode $node) use ($context, &$fieldName) { + NodeKind::BOOLEAN => function (BooleanValueNode $node) use ($context, &$fieldName) : void { $this->isValidScalar($context, $node, $fieldName); }, ]; @@ -177,9 +180,13 @@ public static function badValueMessage($typeName, $valueName, $message = null) ($message ? "; ${message}" : '.'); } + /** + * @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode $node + */ private function isValidScalar(ValidationContext $context, ValueNode $node, $fieldName) { // Report any error at the full type expected by the location. + /** @var ScalarType|EnumType|InputObjectType|ListOfType|NonNull $locationType */ $locationType = $context->getInputType(); if (! $locationType) { @@ -209,20 +216,6 @@ private function isValidScalar(ValidationContext $context, ValueNode $node, $fie // may throw to indicate failure. try { $type->parseLiteral($node); - } catch (Exception $error) { - // Ensure a reference to the original error is maintained. - $context->reportError( - new Error( - self::getBadValueMessage( - (string) $locationType, - Printer::doPrint($node), - $error->getMessage(), - $context, - $fieldName - ), - $node - ) - ); } catch (Throwable $error) { // Ensure a reference to the original error is maintained. $context->reportError( @@ -234,19 +227,26 @@ private function isValidScalar(ValidationContext $context, ValueNode $node, $fie $context, $fieldName ), - $node + $node, + null, + [], + null, + $error ) ); } } + /** + * @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode $node + */ private function enumTypeSuggestion($type, ValueNode $node) { if ($type instanceof EnumType) { $suggestions = Utils::suggestionList( Printer::doPrint($node), array_map( - static function (EnumValueDefinition $value) { + static function (EnumValueDefinition $value) : string { return $value->name; }, $type->getValues() diff --git a/src/Validator/Rules/VariablesAreInputTypes.php b/src/Validator/Rules/VariablesAreInputTypes.php index 5dc27b433..a1daafff0 100644 --- a/src/Validator/Rules/VariablesAreInputTypes.php +++ b/src/Validator/Rules/VariablesAreInputTypes.php @@ -18,7 +18,7 @@ class VariablesAreInputTypes extends ValidationRule public function getVisitor(ValidationContext $context) { return [ - NodeKind::VARIABLE_DEFINITION => static function (VariableDefinitionNode $node) use ($context) { + NodeKind::VARIABLE_DEFINITION => static function (VariableDefinitionNode $node) use ($context) : void { $type = TypeInfo::typeFromAST($context->getSchema(), $node->type); // If the variable type is not an input type, return an error. diff --git a/src/Validator/Rules/VariablesDefaultValueAllowed.php b/src/Validator/Rules/VariablesDefaultValueAllowed.php deleted file mode 100644 index a19c14d2f..000000000 --- a/src/Validator/Rules/VariablesDefaultValueAllowed.php +++ /dev/null @@ -1,65 +0,0 @@ - static function (VariableDefinitionNode $node) use ($context) { - $name = $node->variable->name->value; - $defaultValue = $node->defaultValue; - $type = $context->getInputType(); - if ($type instanceof NonNull && $defaultValue) { - $context->reportError( - new Error( - self::defaultForRequiredVarMessage( - $name, - $type, - $type->getWrappedType() - ), - [$defaultValue] - ) - ); - } - - return Visitor::skipNode(); - }, - NodeKind::SELECTION_SET => static function (SelectionSetNode $node) { - return Visitor::skipNode(); - }, - NodeKind::FRAGMENT_DEFINITION => static function (FragmentDefinitionNode $node) { - return Visitor::skipNode(); - }, - ]; - } - - public static function defaultForRequiredVarMessage($varName, $type, $guessType) - { - return sprintf( - 'Variable "$%s" of type "%s" is required and will not use the default value. Perhaps you meant to use type "%s".', - $varName, - $type, - $guessType - ); - } -} diff --git a/src/Validator/Rules/VariablesInAllowedPosition.php b/src/Validator/Rules/VariablesInAllowedPosition.php index 2deb0e964..717bd6563 100644 --- a/src/Validator/Rules/VariablesInAllowedPosition.php +++ b/src/Validator/Rules/VariablesInAllowedPosition.php @@ -6,35 +6,44 @@ use GraphQL\Error\Error; use GraphQL\Language\AST\NodeKind; +use GraphQL\Language\AST\NullValueNode; use GraphQL\Language\AST\OperationDefinitionNode; +use GraphQL\Language\AST\ValueNode; use GraphQL\Language\AST\VariableDefinitionNode; -use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; +use GraphQL\Type\Definition\Type; +use GraphQL\Type\Schema; use GraphQL\Utils\TypeComparators; use GraphQL\Utils\TypeInfo; +use GraphQL\Utils\Utils; use GraphQL\Validator\ValidationContext; use function sprintf; class VariablesInAllowedPosition extends ValidationRule { - /** @var */ + /** + * A map from variable names to their definition nodes. + * + * @var VariableDefinitionNode[] + */ public $varDefMap; public function getVisitor(ValidationContext $context) { return [ NodeKind::OPERATION_DEFINITION => [ - 'enter' => function () { + 'enter' => function () : void { $this->varDefMap = []; }, - 'leave' => function (OperationDefinitionNode $operation) use ($context) { + 'leave' => function (OperationDefinitionNode $operation) use ($context) : void { $usages = $context->getRecursiveVariableUsages($operation); foreach ($usages as $usage) { - $node = $usage['node']; - $type = $usage['type']; - $varName = $node->name->value; - $varDef = $this->varDefMap[$varName] ?? null; + $node = $usage['node']; + $type = $usage['type']; + $defaultValue = $usage['defaultValue']; + $varName = $node->name->value; + $varDef = $this->varDefMap[$varName] ?? null; if ($varDef === null || $type === null) { continue; @@ -48,11 +57,7 @@ public function getVisitor(ValidationContext $context) $schema = $context->getSchema(); $varType = TypeInfo::typeFromAST($schema, $varDef->type); - if (! $varType || TypeComparators::isTypeSubTypeOf( - $schema, - $this->effectiveType($varType, $varDef), - $type - )) { + if (! $varType || $this->allowedVariableUsage($schema, $varType, $varDef->defaultValue, $type, $defaultValue)) { continue; } @@ -63,17 +68,12 @@ public function getVisitor(ValidationContext $context) } }, ], - NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $varDefNode) { + NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $varDefNode) : void { $this->varDefMap[$varDefNode->variable->name->value] = $varDefNode; }, ]; } - private function effectiveType($varType, $varDef) - { - return ! $varDef->defaultValue || $varType instanceof NonNull ? $varType : new NonNull($varType); - } - /** * A var type is allowed if it is the same or more strict than the expected * type. It can be more strict if the variable type is non-null when the @@ -90,23 +90,27 @@ public static function badVarPosMessage($varName, $varType, $expectedType) ); } - /** If a variable definition has a default value, it's effectively non-null. */ - private function varTypeAllowedForType($varType, $expectedType) + /** + * Returns true if the variable is allowed in the location it was found, + * which includes considering if default values exist for either the variable + * or the location at which it is located. + * + * @param ValueNode|null $varDefaultValue + * @param mixed $locationDefaultValue + */ + private function allowedVariableUsage(Schema $schema, Type $varType, $varDefaultValue, Type $locationType, $locationDefaultValue) : bool { - if ($expectedType instanceof NonNull) { - if ($varType instanceof NonNull) { - return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType->getWrappedType()); + if ($locationType instanceof NonNull && ! $varType instanceof NonNull) { + $hasNonNullVariableDefaultValue = $varDefaultValue && ! $varDefaultValue instanceof NullValueNode; + $hasLocationDefaultValue = ! Utils::isInvalid($locationDefaultValue); + if (! $hasNonNullVariableDefaultValue && ! $hasLocationDefaultValue) { + return false; } + $nullableLocationType = $locationType->getWrappedType(); - return false; - } - if ($varType instanceof NonNull) { - return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType); - } - if ($varType instanceof ListOfType && $expectedType instanceof ListOfType) { - return $this->varTypeAllowedForType($varType->getWrappedType(), $expectedType->getWrappedType()); + return TypeComparators::isTypeSubTypeOf($schema, $varType, $nullableLocationType); } - return $varType === $expectedType; + return TypeComparators::isTypeSubTypeOf($schema, $varType, $locationType); } } diff --git a/src/Validator/SDLValidationContext.php b/src/Validator/SDLValidationContext.php new file mode 100644 index 000000000..379515e2c --- /dev/null +++ b/src/Validator/SDLValidationContext.php @@ -0,0 +1,9 @@ +schema = $schema; - $this->ast = $ast; + parent::__construct($ast, $schema); $this->typeInfo = $typeInfo; - $this->errors = []; $this->fragmentSpreads = new SplObjectStorage(); $this->recursivelyReferencedFragments = new SplObjectStorage(); $this->variableUsages = new SplObjectStorage(); $this->recursiveVariableUsages = new SplObjectStorage(); } - public function reportError(Error $error) - { - $this->errors[] = $error; - } - - /** - * @return Error[] - */ - public function getErrors() - { - return $this->errors; - } - - /** - * @return Schema - */ - public function getSchema() - { - return $this->schema; - } - /** * @return mixed[][] List of ['node' => VariableNode, 'type' => ?InputObjectType] */ @@ -102,11 +79,11 @@ public function getRecursiveVariableUsages(OperationDefinitionNode $operation) $usages = $this->getVariableUsages($operation); $fragments = $this->getRecursivelyReferencedFragments($operation); - $tmp = [$usages]; - foreach ($fragments as $i => $fragment) { - $tmp[] = $this->getVariableUsages($fragments[$i]); + $allUsages = [$usages]; + foreach ($fragments as $fragment) { + $allUsages[] = $this->getVariableUsages($fragment); } - $usages = call_user_func_array('array_merge', $tmp); + $usages = array_merge(...$allUsages); $this->recursiveVariableUsages[$operation] = $usages; } @@ -128,14 +105,18 @@ private function getVariableUsages(HasSelectionSet $node) Visitor::visitWithTypeInfo( $typeInfo, [ - NodeKind::VARIABLE_DEFINITION => static function () { + NodeKind::VARIABLE_DEFINITION => static function () : bool { return false; }, NodeKind::VARIABLE => static function (VariableNode $variable) use ( &$newUsages, $typeInfo - ) { - $newUsages[] = ['node' => $variable, 'type' => $typeInfo->getInputType()]; + ) : void { + $newUsages[] = [ + 'node' => $variable, + 'type' => $typeInfo->getInputType(), + 'defaultValue' => $typeInfo->getDefaultValue(), + ]; }, ] ) @@ -158,13 +139,13 @@ public function getRecursivelyReferencedFragments(OperationDefinitionNode $opera $fragments = []; $collectedNames = []; $nodesToVisit = [$operation]; - while (! empty($nodesToVisit)) { + while (count($nodesToVisit) > 0) { $node = array_pop($nodesToVisit); $spreads = $this->getFragmentSpreads($node); foreach ($spreads as $spread) { $fragName = $spread->name->value; - if (! empty($collectedNames[$fragName])) { + if ($collectedNames[$fragName] ?? false) { continue; } @@ -185,24 +166,30 @@ public function getRecursivelyReferencedFragments(OperationDefinitionNode $opera } /** + * @param OperationDefinitionNode|FragmentDefinitionNode $node + * * @return FragmentSpreadNode[] */ - public function getFragmentSpreads(HasSelectionSet $node) + public function getFragmentSpreads(HasSelectionSet $node) : array { $spreads = $this->fragmentSpreads[$node] ?? null; if ($spreads === null) { $spreads = []; /** @var SelectionSetNode[] $setsToVisit */ $setsToVisit = [$node->selectionSet]; - while (! empty($setsToVisit)) { + while (count($setsToVisit) > 0) { $set = array_pop($setsToVisit); for ($i = 0, $selectionCount = count($set->selections); $i < $selectionCount; $i++) { $selection = $set->selections[$i]; - if ($selection->kind === NodeKind::FRAGMENT_SPREAD) { + if ($selection instanceof FragmentSpreadNode) { $spreads[] = $selection; - } elseif ($selection->selectionSet) { - $setsToVisit[] = $selection->selectionSet; + } elseif ($selection instanceof FieldNode || $selection instanceof InlineFragmentNode) { + if ($selection->selectionSet) { + $setsToVisit[] = $selection->selectionSet; + } + } else { + throw InvariantViolation::shouldNotHappen(); } } } @@ -223,7 +210,7 @@ public function getFragment($name) if (! $fragments) { $fragments = []; foreach ($this->getDocument()->definitions as $statement) { - if ($statement->kind !== NodeKind::FRAGMENT_DEFINITION) { + if (! ($statement instanceof FragmentDefinitionNode)) { continue; } @@ -235,44 +222,31 @@ public function getFragment($name) return $fragments[$name] ?? null; } - /** - * @return DocumentNode - */ - public function getDocument() - { - return $this->ast; - } - - /** - * Returns OutputType - * - * @return Type - */ - public function getType() + public function getType() : ?OutputType { return $this->typeInfo->getType(); } /** - * @return Type + * @return (CompositeType & Type) | null */ - public function getParentType() + public function getParentType() : ?CompositeType { return $this->typeInfo->getParentType(); } /** - * @return InputType + * @return (Type & InputType) | null */ - public function getInputType() + public function getInputType() : ?InputType { return $this->typeInfo->getInputType(); } /** - * @return InputType + * @return (Type&InputType)|null */ - public function getParentInputType() + public function getParentInputType() : ?InputType { return $this->typeInfo->getParentInputType(); } diff --git a/tests/Error/ErrorTest.php b/tests/Error/ErrorTest.php index eb0a6c8af..9486ce90b 100644 --- a/tests/Error/ErrorTest.php +++ b/tests/Error/ErrorTest.php @@ -6,6 +6,7 @@ use Exception; use GraphQL\Error\Error; +use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\Parser; use GraphQL\Language\Source; use GraphQL\Language\SourceLocation; @@ -19,7 +20,7 @@ class ErrorTest extends TestCase public function testUsesTheStackOfAnOriginalError() : void { $prev = new Exception('Original'); - $err = new Error('msg', null, null, null, null, $prev); + $err = new Error('msg', null, null, [], null, $prev); self::assertSame($err->getPrevious(), $prev); } @@ -29,12 +30,14 @@ public function testUsesTheStackOfAnOriginalError() : void */ public function testConvertsNodesToPositionsAndLocations() : void { - $source = new Source('{ + $source = new Source('{ field }'); - $ast = Parser::parse($source); - $fieldNode = $ast->definitions[0]->selectionSet->selections[0]; - $e = new Error('msg', [$fieldNode]); + $ast = Parser::parse($source); + /** @var OperationDefinitionNode $operationDefinition */ + $operationDefinition = $ast->definitions[0]; + $fieldNode = $operationDefinition->selectionSet->selections[0]; + $e = new Error('msg', [$fieldNode]); self::assertEquals([$fieldNode], $e->nodes); self::assertEquals($source, $e->getSource()); @@ -47,12 +50,14 @@ public function testConvertsNodesToPositionsAndLocations() : void */ public function testConvertSingleNodeToPositionsAndLocations() : void { - $source = new Source('{ + $source = new Source('{ field }'); - $ast = Parser::parse($source); - $fieldNode = $ast->definitions[0]->selectionSet->selections[0]; - $e = new Error('msg', $fieldNode); // Non-array value. + $ast = Parser::parse($source); + /** @var OperationDefinitionNode $operationDefinition */ + $operationDefinition = $ast->definitions[0]; + $fieldNode = $operationDefinition->selectionSet->selections[0]; + $e = new Error('msg', $fieldNode); // Non-array value. self::assertEquals([$fieldNode], $e->nodes); self::assertEquals($source, $e->getSource()); @@ -108,8 +113,11 @@ public function testSerializesToIncludeMessage() : void */ public function testSerializesToIncludeMessageAndLocations() : void { - $node = Parser::parse('{ field }')->definitions[0]->selectionSet->selections[0]; - $e = new Error('msg', [$node]); + $ast = Parser::parse('{ field }'); + /** @var OperationDefinitionNode $operationDefinition */ + $operationDefinition = $ast->definitions[0]; + $node = $operationDefinition->selectionSet->selections[0]; + $e = new Error('msg', [$node]); self::assertEquals( ['message' => 'msg', 'locations' => [['line' => 1, 'column' => 3]]], @@ -126,7 +134,7 @@ public function testSerializesToIncludePath() : void 'msg', null, null, - null, + [], ['path', 3, 'to', 'field'] ); @@ -143,7 +151,7 @@ public function testDefaultErrorFormatterIncludesExtensionFields() : void 'msg', null, null, - null, + [], null, null, ['foo' => 'bar'] diff --git a/tests/Error/PrintErrorTest.php b/tests/Error/PrintErrorTest.php index ce8a2c111..f8839f8c5 100644 --- a/tests/Error/PrintErrorTest.php +++ b/tests/Error/PrintErrorTest.php @@ -6,6 +6,7 @@ use GraphQL\Error\Error; use GraphQL\Error\FormattedError; +use GraphQL\Language\AST\ObjectTypeDefinitionNode; use GraphQL\Language\Parser; use GraphQL\Language\Source; use GraphQL\Language\SourceLocation; @@ -63,7 +64,9 @@ public function testPrintsAnErrorWithNodesFromDifferentSources() : void 'SourceA' )); - $fieldTypeA = $sourceA->definitions[0]->fields[0]->type; + /** @var ObjectTypeDefinitionNode $objectDefinitionA */ + $objectDefinitionA = $sourceA->definitions[0]; + $fieldTypeA = $objectDefinitionA->fields[0]->type; $sourceB = Parser::parse(new Source( 'type Foo { @@ -72,7 +75,9 @@ public function testPrintsAnErrorWithNodesFromDifferentSources() : void 'SourceB' )); - $fieldTypeB = $sourceB->definitions[0]->fields[0]->type; + /** @var ObjectTypeDefinitionNode $objectDefinitionB */ + $objectDefinitionB = $sourceB->definitions[0]; + $fieldTypeB = $objectDefinitionB->fields[0]->type; $error = new Error( 'Example error with two nodes', diff --git a/tests/Exception/InvalidArgumentTest.php b/tests/Exception/InvalidArgumentTest.php new file mode 100644 index 000000000..381c5ee12 --- /dev/null +++ b/tests/Exception/InvalidArgumentTest.php @@ -0,0 +1,18 @@ +getMessage()); + } +} diff --git a/tests/Executor/AbstractPromiseTest.php b/tests/Executor/AbstractPromiseTest.php index ed39d04e1..219635b92 100644 --- a/tests/Executor/AbstractPromiseTest.php +++ b/tests/Executor/AbstractPromiseTest.php @@ -5,11 +5,13 @@ namespace GraphQL\Tests\Executor; use GraphQL\Deferred; +use GraphQL\Error\DebugFlag; use GraphQL\Error\UserError; use GraphQL\GraphQL; use GraphQL\Tests\Executor\TestClasses\Cat; use GraphQL\Tests\Executor\TestClasses\Dog; use GraphQL\Tests\Executor\TestClasses\Human; +use GraphQL\Tests\PHPUnit\ArraySubsetAsserts; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; @@ -22,12 +24,14 @@ */ class AbstractPromiseTest extends TestCase { + use ArraySubsetAsserts; + /** * @see it('isTypeOf used to resolve runtime type for Interface') */ public function testIsTypeOfUsedToResolveRuntimeTypeForInterface() : void { - $PetType = new InterfaceType([ + $petType = new InterfaceType([ 'name' => 'Pet', 'fields' => [ 'name' => ['type' => Type::string()], @@ -36,9 +40,9 @@ public function testIsTypeOfUsedToResolveRuntimeTypeForInterface() : void $DogType = new ObjectType([ 'name' => 'Dog', - 'interfaces' => [$PetType], + 'interfaces' => [$petType], 'isTypeOf' => static function ($obj) { - return new Deferred(static function () use ($obj) { + return new Deferred(static function () use ($obj) : bool { return $obj instanceof Dog; }); }, @@ -50,9 +54,9 @@ public function testIsTypeOfUsedToResolveRuntimeTypeForInterface() : void $CatType = new ObjectType([ 'name' => 'Cat', - 'interfaces' => [$PetType], + 'interfaces' => [$petType], 'isTypeOf' => static function ($obj) { - return new Deferred(static function () use ($obj) { + return new Deferred(static function () use ($obj) : bool { return $obj instanceof Cat; }); }, @@ -67,8 +71,8 @@ public function testIsTypeOfUsedToResolveRuntimeTypeForInterface() : void 'name' => 'Query', 'fields' => [ 'pets' => [ - 'type' => Type::listOf($PetType), - 'resolve' => static function () { + 'type' => Type::listOf($petType), + 'resolve' => static function () : array { return [ new Dog('Odie', true), new Cat('Garfield', false), @@ -121,8 +125,8 @@ public function testIsTypeOfCanBeRejected() : void $DogType = new ObjectType([ 'name' => 'Dog', 'interfaces' => [$PetType], - 'isTypeOf' => static function () { - return new Deferred(static function () { + 'isTypeOf' => static function () : Deferred { + return new Deferred(static function () : void { throw new UserError('We are testing this error'); }); }, @@ -136,7 +140,7 @@ public function testIsTypeOfCanBeRejected() : void 'name' => 'Cat', 'interfaces' => [$PetType], 'isTypeOf' => static function ($obj) { - return new Deferred(static function () use ($obj) { + return new Deferred(static function () use ($obj) : bool { return $obj instanceof Cat; }); }, @@ -152,7 +156,7 @@ public function testIsTypeOfCanBeRejected() : void 'fields' => [ 'pets' => [ 'type' => Type::listOf($PetType), - 'resolve' => static function () { + 'resolve' => static function () : array { return [ new Dog('Odie', true), new Cat('Garfield', false), @@ -204,10 +208,10 @@ public function testIsTypeOfCanBeRejected() : void */ public function testIsTypeOfUsedToResolveRuntimeTypeForUnion() : void { - $DogType = new ObjectType([ + $dogType = new ObjectType([ 'name' => 'Dog', 'isTypeOf' => static function ($obj) { - return new Deferred(static function () use ($obj) { + return new Deferred(static function () use ($obj) : bool { return $obj instanceof Dog; }); }, @@ -217,10 +221,10 @@ public function testIsTypeOfUsedToResolveRuntimeTypeForUnion() : void ], ]); - $CatType = new ObjectType([ + $catType = new ObjectType([ 'name' => 'Cat', 'isTypeOf' => static function ($obj) { - return new Deferred(static function () use ($obj) { + return new Deferred(static function () use ($obj) : bool { return $obj instanceof Cat; }); }, @@ -230,9 +234,9 @@ public function testIsTypeOfUsedToResolveRuntimeTypeForUnion() : void ], ]); - $PetType = new UnionType([ + $petType = new UnionType([ 'name' => 'Pet', - 'types' => [$DogType, $CatType], + 'types' => [$dogType, $catType], ]); $schema = new Schema([ @@ -240,8 +244,8 @@ public function testIsTypeOfUsedToResolveRuntimeTypeForUnion() : void 'name' => 'Query', 'fields' => [ 'pets' => [ - 'type' => Type::listOf($PetType), - 'resolve' => static function () { + 'type' => Type::listOf($petType), + 'resolve' => static function () : array { return [new Dog('Odie', true), new Cat('Garfield', false)]; }, ], @@ -283,8 +287,8 @@ public function testResolveTypeOnInterfaceYieldsUsefulError() : void { $PetType = new InterfaceType([ 'name' => 'Pet', - 'resolveType' => static function ($obj) use (&$DogType, &$CatType, &$HumanType) { - return new Deferred(static function () use ($obj, $DogType, $CatType, $HumanType) { + 'resolveType' => static function ($obj) use (&$DogType, &$CatType, &$HumanType) : Deferred { + return new Deferred(static function () use ($obj, $DogType, $CatType, $HumanType) : ?Type { if ($obj instanceof Dog) { return $DogType; } @@ -335,7 +339,7 @@ public function testResolveTypeOnInterfaceYieldsUsefulError() : void 'pets' => [ 'type' => Type::listOf($PetType), 'resolve' => static function () { - return new Deferred(static function () { + return new Deferred(static function () : array { return [ new Dog('Odie', true), new Cat('Garfield', false), @@ -361,7 +365,7 @@ public function testResolveTypeOnInterfaceYieldsUsefulError() : void } }'; - $result = GraphQL::executeQuery($schema, $query)->toArray(true); + $result = GraphQL::executeQuery($schema, $query)->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE); $expected = [ 'data' => [ @@ -414,7 +418,7 @@ public function testResolveTypeOnUnionYieldsUsefulError() : void $PetType = new UnionType([ 'name' => 'Pet', 'resolveType' => static function ($obj) use ($DogType, $CatType, $HumanType) { - return new Deferred(static function () use ($obj, $DogType, $CatType, $HumanType) { + return new Deferred(static function () use ($obj, $DogType, $CatType, $HumanType) : ?Type { if ($obj instanceof Dog) { return $DogType; } @@ -437,7 +441,7 @@ public function testResolveTypeOnUnionYieldsUsefulError() : void 'fields' => [ 'pets' => [ 'type' => Type::listOf($PetType), - 'resolve' => static function () { + 'resolve' => static function () : array { return [ new Dog('Odie', true), new Cat('Garfield', false), @@ -462,7 +466,7 @@ public function testResolveTypeOnUnionYieldsUsefulError() : void } }'; - $result = GraphQL::executeQuery($schema, $query)->toArray(true); + $result = GraphQL::executeQuery($schema, $query)->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE); $expected = [ 'data' => [ @@ -532,7 +536,7 @@ public function testResolveTypeAllowsResolvingWithTypeName() : void 'fields' => [ 'pets' => [ 'type' => Type::listOf($PetType), - 'resolve' => static function () { + 'resolve' => static function () : array { return [ new Dog('Odie', true), new Cat('Garfield', false), @@ -576,8 +580,8 @@ public function testResolveTypeCanBeCaught() : void { $PetType = new InterfaceType([ 'name' => 'Pet', - 'resolveType' => static function () { - return new Deferred(static function () { + 'resolveType' => static function () : Deferred { + return new Deferred(static function () : void { throw new UserError('We are testing this error'); }); }, @@ -610,7 +614,7 @@ public function testResolveTypeCanBeCaught() : void 'fields' => [ 'pets' => [ 'type' => Type::listOf($PetType), - 'resolve' => static function () { + 'resolve' => static function () : array { return [ new Dog('Odie', true), new Cat('Garfield', false), diff --git a/tests/Executor/AbstractTest.php b/tests/Executor/AbstractTest.php index eba24ecf3..1c3f57899 100644 --- a/tests/Executor/AbstractTest.php +++ b/tests/Executor/AbstractTest.php @@ -4,6 +4,8 @@ namespace GraphQL\Tests\Executor; +use GraphQL\Error\DebugFlag; +use GraphQL\Error\InvariantViolation; use GraphQL\Executor\ExecutionResult; use GraphQL\Executor\Executor; use GraphQL\GraphQL; @@ -11,6 +13,7 @@ use GraphQL\Tests\Executor\TestClasses\Cat; use GraphQL\Tests\Executor\TestClasses\Dog; use GraphQL\Tests\Executor\TestClasses\Human; +use GraphQL\Tests\PHPUnit\ArraySubsetAsserts; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; @@ -23,6 +26,8 @@ */ class AbstractTest extends TestCase { + use ArraySubsetAsserts; + /** * @see it('isTypeOf used to resolve runtime type for Interface') */ @@ -40,7 +45,7 @@ public function testIsTypeOfUsedToResolveRuntimeTypeForInterface() : void $dogType = new ObjectType([ 'name' => 'Dog', 'interfaces' => [$petType], - 'isTypeOf' => static function ($obj) { + 'isTypeOf' => static function ($obj) : bool { return $obj instanceof Dog; }, 'fields' => [ @@ -52,7 +57,7 @@ public function testIsTypeOfUsedToResolveRuntimeTypeForInterface() : void $catType = new ObjectType([ 'name' => 'Cat', 'interfaces' => [$petType], - 'isTypeOf' => static function ($obj) { + 'isTypeOf' => static function ($obj) : bool { return $obj instanceof Cat; }, 'fields' => [ @@ -67,7 +72,7 @@ public function testIsTypeOfUsedToResolveRuntimeTypeForInterface() : void 'fields' => [ 'pets' => [ 'type' => Type::listOf($petType), - 'resolve' => static function () { + 'resolve' => static function () : array { return [new Dog('Odie', true), new Cat('Garfield', false)]; }, ], @@ -106,7 +111,7 @@ public function testIsTypeOfUsedToResolveRuntimeTypeForUnion() : void { $dogType = new ObjectType([ 'name' => 'Dog', - 'isTypeOf' => static function ($obj) { + 'isTypeOf' => static function ($obj) : bool { return $obj instanceof Dog; }, 'fields' => [ @@ -117,7 +122,7 @@ public function testIsTypeOfUsedToResolveRuntimeTypeForUnion() : void $catType = new ObjectType([ 'name' => 'Cat', - 'isTypeOf' => static function ($obj) { + 'isTypeOf' => static function ($obj) : bool { return $obj instanceof Cat; }, 'fields' => [ @@ -137,7 +142,7 @@ public function testIsTypeOfUsedToResolveRuntimeTypeForUnion() : void 'fields' => [ 'pets' => [ 'type' => Type::listOf($petType), - 'resolve' => static function () { + 'resolve' => static function () : array { return [new Dog('Odie', true), new Cat('Garfield', false)]; }, ], @@ -227,7 +232,7 @@ public function testResolveTypeOnInterfaceYieldsUsefulError() : void 'fields' => [ 'pets' => [ 'type' => Type::listOf($PetType), - 'resolve' => static function () { + 'resolve' => static function () : array { return [ new Dog('Odie', true), new Cat('Garfield', false), @@ -267,7 +272,7 @@ public function testResolveTypeOnInterfaceYieldsUsefulError() : void ], ], ]; - $actual = GraphQL::executeQuery($schema, $query)->toArray(true); + $actual = GraphQL::executeQuery($schema, $query)->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE); self::assertArraySubset($expected, $actual); } @@ -302,7 +307,7 @@ public function testResolveTypeOnUnionYieldsUsefulError() : void $PetType = new UnionType([ 'name' => 'Pet', - 'resolveType' => static function ($obj) use ($DogType, $CatType, $HumanType) { + 'resolveType' => static function ($obj) use ($DogType, $CatType, $HumanType) : Type { if ($obj instanceof Dog) { return $DogType; } @@ -312,6 +317,8 @@ public function testResolveTypeOnUnionYieldsUsefulError() : void if ($obj instanceof Human) { return $HumanType; } + + throw new InvariantViolation('Invalid type'); }, 'types' => [$DogType, $CatType], ]); @@ -322,7 +329,7 @@ public function testResolveTypeOnUnionYieldsUsefulError() : void 'fields' => [ 'pets' => [ 'type' => Type::listOf($PetType), - 'resolve' => static function () { + 'resolve' => static function () : array { return [ new Dog('Odie', true), new Cat('Garfield', false), @@ -347,7 +354,7 @@ public function testResolveTypeOnUnionYieldsUsefulError() : void } }'; - $result = GraphQL::executeQuery($schema, $query)->toArray(true); + $result = GraphQL::executeQuery($schema, $query)->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE); $expected = [ 'data' => [ 'pets' => [ @@ -380,7 +387,7 @@ public function testReturningInvalidValueFromResolveTypeYieldsUsefulError() : vo $fooInterface = new InterfaceType([ 'name' => 'FooInterface', 'fields' => ['bar' => ['type' => Type::string()]], - 'resolveType' => static function () { + 'resolveType' => static function () : array { return []; }, ]); @@ -397,7 +404,7 @@ public function testReturningInvalidValueFromResolveTypeYieldsUsefulError() : vo 'fields' => [ 'foo' => [ 'type' => $fooInterface, - 'resolve' => static function () { + 'resolve' => static function () : string { return 'dummy'; }, ], @@ -424,7 +431,7 @@ public function testReturningInvalidValueFromResolveTypeYieldsUsefulError() : vo ], ], ]; - self::assertEquals($expected, $result->toArray(true)); + self::assertEquals($expected, $result->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)); } /** @@ -473,7 +480,7 @@ public function testResolveTypeAllowsResolvingWithTypeName() : void 'fields' => [ 'pets' => [ 'type' => Type::listOf($PetType), - 'resolve' => static function () { + 'resolve' => static function () : array { return [ new Dog('Odie', true), new Cat('Garfield', false), @@ -520,7 +527,7 @@ public function testHintsOnConflictingTypeInstancesInResolveType() : void 'fields' => [ 'a' => Type::string(), ], - 'interfaces' => static function () use ($iface) { + 'interfaces' => static function () use ($iface) : array { return [$iface]; }, ]); diff --git a/tests/Executor/DeferredFieldsTest.php b/tests/Executor/DeferredFieldsTest.php index e68388c24..2f301c5af 100644 --- a/tests/Executor/DeferredFieldsTest.php +++ b/tests/Executor/DeferredFieldsTest.php @@ -28,7 +28,7 @@ class DeferredFieldsTest extends TestCase /** @var ObjectType */ private $categoryType; - /** @var */ + /** @var mixed */ private $paths; /** @var mixed[][] */ @@ -43,7 +43,7 @@ class DeferredFieldsTest extends TestCase /** @var ObjectType */ private $queryType; - public function setUp() + public function setUp() : void { $this->storyDataSource = [ ['id' => 1, 'authorId' => 1, 'title' => 'Story #1', 'categoryIds' => [2, 3]], @@ -91,12 +91,7 @@ public function setUp() return new Deferred(function () use ($user) { $this->paths[] = 'deferred-for-best-friend-of-' . $user['id']; - return Utils::find( - $this->userDataSource, - static function ($entry) use ($user) { - return $entry['id'] === $user['bestFriendId']; - } - ); + return $this->findUserById($user['bestFriendId']); }); }, ], @@ -109,10 +104,10 @@ static function ($entry) use ($user) { 'fields' => [ 'title' => [ 'type' => Type::string(), - 'resolve' => function ($entry, $args, $context, ResolveInfo $info) { + 'resolve' => function ($story, $args, $context, ResolveInfo $info) { $this->paths[] = $info->path; - return $entry['title']; + return $story['title']; }, ], 'author' => [ @@ -123,12 +118,7 @@ static function ($entry) use ($user) { return new Deferred(function () use ($story) { $this->paths[] = 'deferred-for-story-' . $story['id'] . '-author'; - return Utils::find( - $this->userDataSource, - static function ($entry) use ($story) { - return $entry['id'] === $story['authorId']; - } - ); + return $this->findUserById($story['authorId']); }); }, ], @@ -154,7 +144,7 @@ static function ($entry) use ($story) { return Utils::filter( $this->storyDataSource, - static function ($story) use ($category) { + static function ($story) use ($category) : bool { return in_array($category['id'], $story['categoryIds'], true); } ); @@ -168,12 +158,24 @@ static function ($story) use ($category) { return new Deferred(function () use ($category) { $this->paths[] = 'deferred-for-category-' . $category['id'] . '-topStory'; - return Utils::find( - $this->storyDataSource, - static function ($story) use ($category) { - return $story['id'] === $category['topStoryId']; - } - ); + return $this->findStoryById($category['topStoryId']); + }); + }, + ], + 'topStoryAuthor' => [ + 'type' => $this->userType, + 'resolve' => function ($category, $args, $context, ResolveInfo $info) { + $this->paths[] = $info->path; + + return new Deferred(function () use ($category) { + $this->paths[] = 'deferred-for-category-' . $category['id'] . '-topStoryAuthor1'; + $story = $this->findStoryById($category['topStoryId']); + + return new Deferred(function () use ($category, $story) { + $this->paths[] = 'deferred-for-category-' . $category['id'] . '-topStoryAuthor2'; + + return $this->findUserById($story['authorId']); + }); }); }, ], @@ -185,12 +187,12 @@ static function ($story) use ($category) { 'fields' => [ 'topStories' => [ 'type' => Type::listOf($this->storyType), - 'resolve' => function ($val, $args, $context, ResolveInfo $info) { + 'resolve' => function ($rootValue, $args, $context, ResolveInfo $info) { $this->paths[] = $info->path; return Utils::filter( $this->storyDataSource, - static function ($story) { + static function ($story) : bool { return $story['id'] % 2 === 1; } ); @@ -198,7 +200,7 @@ static function ($story) { ], 'featuredCategory' => [ 'type' => $this->categoryType, - 'resolve' => function ($val, $args, $context, ResolveInfo $info) { + 'resolve' => function ($rootValue, $args, $context, ResolveInfo $info) : array { $this->paths[] = $info->path; return $this->categoryDataSource[0]; @@ -206,7 +208,7 @@ static function ($story) { ], 'categories' => [ 'type' => Type::listOf($this->categoryType), - 'resolve' => function ($val, $args, $context, ResolveInfo $info) { + 'resolve' => function ($rootValue, $args, $context, ResolveInfo $info) : array { $this->paths[] = $info->path; return $this->categoryDataSource; @@ -401,7 +403,7 @@ public function testComplexRecursiveDeferredFields() : void return [ 'sync' => [ 'type' => Type::string(), - 'resolve' => function ($v, $a, $c, ResolveInfo $info) { + 'resolve' => function ($complexType, $args, $context, ResolveInfo $info) : string { $this->paths[] = $info->path; return 'sync'; @@ -409,10 +411,10 @@ public function testComplexRecursiveDeferredFields() : void ], 'deferred' => [ 'type' => Type::string(), - 'resolve' => function ($v, $a, $c, ResolveInfo $info) { + 'resolve' => function ($complexType, $args, $context, ResolveInfo $info) { $this->paths[] = $info->path; - return new Deferred(function () use ($info) { + return new Deferred(function () use ($info) : string { $this->paths[] = ['!dfd for: ', $info->path]; return 'deferred'; @@ -421,7 +423,7 @@ public function testComplexRecursiveDeferredFields() : void ], 'nest' => [ 'type' => $complexType, - 'resolve' => function ($v, $a, $c, ResolveInfo $info) { + 'resolve' => function ($complexType, $args, $context, ResolveInfo $info) : array { $this->paths[] = $info->path; return []; @@ -429,10 +431,10 @@ public function testComplexRecursiveDeferredFields() : void ], 'deferredNest' => [ 'type' => $complexType, - 'resolve' => function ($v, $a, $c, ResolveInfo $info) { + 'resolve' => function ($complexType, $args, $context, ResolveInfo $info) { $this->paths[] = $info->path; - return new Deferred(function () use ($info) { + return new Deferred(function () use ($info) : array { $this->paths[] = ['!dfd nest for: ', $info->path]; return []; @@ -539,9 +541,113 @@ public function testComplexRecursiveDeferredFields() : void ['!dfd for: ', ['deferredNest', 'deferredNest', 'deferred']], ]; + // Note: not using self::assertEquals() because coroutineExecutor has a different sequence of calls self::assertCount(count($expectedPaths), $this->paths); foreach ($expectedPaths as $expectedPath) { self::assertTrue(in_array($expectedPath, $this->paths, true), 'Missing path: ' . json_encode($expectedPath)); } } + + public function testDeferredChaining() + { + $schema = new Schema([ + 'query' => $this->queryType, + ]); + + $query = Parser::parse(' + { + categories { + name + topStory { + title + author { + name + } + } + topStoryAuthor { + name + } + } + } + '); + + $author1 = ['name' => 'John'/*, 'bestFriend' => ['name' => 'Dirk']*/]; + $author2 = ['name' => 'Jane'/*, 'bestFriend' => ['name' => 'Joe']*/]; + $author3 = ['name' => 'Joe'/*, 'bestFriend' => ['name' => 'Jane']*/]; + $author4 = ['name' => 'Dirk'/*, 'bestFriend' => ['name' => 'John']*/]; + + $story1 = ['title' => 'Story #8', 'author' => $author1]; + $story2 = ['title' => 'Story #3', 'author' => $author3]; + $story3 = ['title' => 'Story #9', 'author' => $author2]; + + $result = Executor::execute($schema, $query); + $expected = [ + 'data' => [ + 'categories' => [ + ['name' => 'Category #1', 'topStory' => $story1, 'topStoryAuthor' => $author1], + ['name' => 'Category #2', 'topStory' => $story2, 'topStoryAuthor' => $author3], + ['name' => 'Category #3', 'topStory' => $story3, 'topStoryAuthor' => $author2], + ], + ], + ]; + self::assertEquals($expected, $result->toArray()); + + $expectedPaths = [ + ['categories'], + ['categories', 0, 'name'], + ['categories', 0, 'topStory'], + ['categories', 0, 'topStoryAuthor'], + ['categories', 1, 'name'], + ['categories', 1, 'topStory'], + ['categories', 1, 'topStoryAuthor'], + ['categories', 2, 'name'], + ['categories', 2, 'topStory'], + ['categories', 2, 'topStoryAuthor'], + 'deferred-for-category-1-topStory', + 'deferred-for-category-1-topStoryAuthor1', + 'deferred-for-category-2-topStory', + 'deferred-for-category-2-topStoryAuthor1', + 'deferred-for-category-3-topStory', + 'deferred-for-category-3-topStoryAuthor1', + ['categories', 0, 'topStory', 'title'], + ['categories', 0, 'topStory', 'author'], + 'deferred-for-category-1-topStoryAuthor2', + ['categories', 1, 'topStory', 'title'], + ['categories', 1, 'topStory', 'author'], + 'deferred-for-category-2-topStoryAuthor2', + ['categories', 2, 'topStory', 'title'], + ['categories', 2, 'topStory', 'author'], + 'deferred-for-category-3-topStoryAuthor2', + 'deferred-for-story-8-author', + 'deferred-for-story-3-author', + 'deferred-for-story-9-author', + ['categories', 0, 'topStory', 'author', 'name'], + ['categories', 0, 'topStoryAuthor', 'name'], + ['categories', 1, 'topStory', 'author', 'name'], + ['categories', 1, 'topStoryAuthor', 'name'], + ['categories', 2, 'topStory', 'author', 'name'], + ['categories', 2, 'topStoryAuthor', 'name'], + ]; + self::assertEquals($expectedPaths, $this->paths); + } + + private function findStoryById($id) + { + return Utils::find( + $this->storyDataSource, + static function ($story) use ($id) : bool { + return $story['id'] === $id; + } + ); + } + + private function findUserById($id) + { + return Utils::find( + $this->userDataSource, + static function ($user) use ($id) : bool { + return $user['id'] === $id; + } + ); + } } diff --git a/tests/Executor/DirectivesTest.php b/tests/Executor/DirectivesTest.php index 84aa491c8..fda71bb43 100644 --- a/tests/Executor/DirectivesTest.php +++ b/tests/Executor/DirectivesTest.php @@ -20,8 +20,11 @@ class DirectivesTest extends TestCase /** @var Schema */ private static $schema; - /** @var string[] */ - private static $data; + /** @var array */ + private static $data = [ + 'a' => 'a', + 'b' => 'b', + ]; /** * @see it('basic query works') @@ -38,7 +41,7 @@ public function testWorksWithoutDirectives() : void */ private function executeTestQuery($doc) : array { - return Executor::execute(self::getSchema(), Parser::parse($doc), self::getData())->toArray(); + return Executor::execute(self::getSchema(), Parser::parse($doc), self::$data)->toArray(); } private static function getSchema() : Schema @@ -58,17 +61,6 @@ private static function getSchema() : Schema return self::$schema; } - /** - * @return string[] - */ - private static function getData() : array - { - return self::$data ?: (self::$data = [ - 'a' => 'a', - 'b' => 'b', - ]); - } - public function testWorksOnScalars() : void { // if true includes scalar diff --git a/tests/Executor/ExecutionResultTest.php b/tests/Executor/ExecutionResultTest.php index 56758b1b5..9bd3f2802 100644 --- a/tests/Executor/ExecutionResultTest.php +++ b/tests/Executor/ExecutionResultTest.php @@ -4,6 +4,7 @@ namespace GraphQL\Tests\Executor; +use GraphQL\Error\Error; use GraphQL\Executor\ExecutionResult; use PHPUnit\Framework\TestCase; @@ -13,17 +14,29 @@ public function testToArrayWithoutExtensions() : void { $executionResult = new ExecutionResult(); - self::assertEquals([], $executionResult->toArray()); + self::assertSame([], $executionResult->toArray()); } public function testToArrayExtensions() : void { $executionResult = new ExecutionResult(null, [], ['foo' => 'bar']); - self::assertEquals(['extensions' => ['foo' => 'bar']], $executionResult->toArray()); + self::assertSame(['extensions' => ['foo' => 'bar']], $executionResult->toArray()); $executionResult->extensions = ['bar' => 'foo']; - self::assertEquals(['extensions' => ['bar' => 'foo']], $executionResult->toArray()); + self::assertSame(['extensions' => ['bar' => 'foo']], $executionResult->toArray()); + } + + public function testNoEmptyErrors() : void + { + $executionResult = new ExecutionResult(null, [new Error()]); + $executionResult->setErrorsHandler( + static function () : array { + return []; + } + ); + + self::assertSame([], $executionResult->toArray()); } } diff --git a/tests/Executor/ExecutorLazySchemaTest.php b/tests/Executor/ExecutorLazySchemaTest.php index 245b95df4..2df23e907 100644 --- a/tests/Executor/ExecutorLazySchemaTest.php +++ b/tests/Executor/ExecutorLazySchemaTest.php @@ -4,6 +4,7 @@ namespace GraphQL\Tests\Executor; +use GraphQL\Error\DebugFlag; use GraphQL\Error\InvariantViolation; use GraphQL\Error\Warning; use GraphQL\Executor\ExecutionResult; @@ -64,7 +65,7 @@ public function testWarnsAboutSlowIsTypeOfForLazySchema() : void // isTypeOf used to resolve runtime type for Interface $petType = new InterfaceType([ 'name' => 'Pet', - 'fields' => static function () { + 'fields' => static function () : array { return [ 'name' => ['type' => Type::string()], ]; @@ -75,10 +76,10 @@ public function testWarnsAboutSlowIsTypeOfForLazySchema() : void $dogType = new ObjectType([ 'name' => 'Dog', 'interfaces' => [$petType], - 'isTypeOf' => static function ($obj) { + 'isTypeOf' => static function ($obj) : bool { return $obj instanceof Dog; }, - 'fields' => static function () { + 'fields' => static function () : array { return [ 'name' => ['type' => Type::string()], 'woofs' => ['type' => Type::boolean()], @@ -89,10 +90,10 @@ public function testWarnsAboutSlowIsTypeOfForLazySchema() : void $catType = new ObjectType([ 'name' => 'Cat', 'interfaces' => [$petType], - 'isTypeOf' => static function ($obj) { + 'isTypeOf' => static function ($obj) : bool { return $obj instanceof Cat; }, - 'fields' => static function () { + 'fields' => static function () : array { return [ 'name' => ['type' => Type::string()], 'meows' => ['type' => Type::boolean()], @@ -106,7 +107,7 @@ public function testWarnsAboutSlowIsTypeOfForLazySchema() : void 'fields' => [ 'pets' => [ 'type' => Type::listOf($petType), - 'resolve' => static function () { + 'resolve' => static function () : array { return [new Dog('Odie', true), new Cat('Garfield', false)]; }, ], @@ -171,7 +172,7 @@ public function testHintsOnConflictingTypeInstancesInDefinitions() : void case 'Test': return new ObjectType([ 'name' => 'Test', - 'fields' => static function () { + 'fields' => static function () : array { return [ 'test' => Type::string(), ]; @@ -184,7 +185,7 @@ public function testHintsOnConflictingTypeInstancesInDefinitions() : void $query = new ObjectType([ 'name' => 'Query', - 'fields' => static function () use ($typeLoader) { + 'fields' => static function () use ($typeLoader) : array { return [ 'test' => $typeLoader('Test'), ]; @@ -244,7 +245,7 @@ public function testSimpleQuery() : void 'SomeObject', 'SomeObject.fields', ]; - self::assertEquals($expected, $result->toArray(true)); + self::assertEquals($expected, $result->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)); self::assertEquals($expectedExecutorCalls, $this->calls); } @@ -257,9 +258,9 @@ public function loadType($name, $isExecutorCall = false) switch ($name) { case 'Query': - return $this->queryType ?: $this->queryType = new ObjectType([ + return $this->queryType ?? $this->queryType = new ObjectType([ 'name' => 'Query', - 'fields' => function () { + 'fields' => function () : array { $this->calls[] = 'Query.fields'; return [ @@ -269,9 +270,9 @@ public function loadType($name, $isExecutorCall = false) }, ]); case 'SomeObject': - return $this->someObjectType ?: $this->someObjectType = new ObjectType([ + return $this->someObjectType ?? $this->someObjectType = new ObjectType([ 'name' => 'SomeObject', - 'fields' => function () { + 'fields' => function () : array { $this->calls[] = 'SomeObject.fields'; return [ @@ -279,7 +280,7 @@ public function loadType($name, $isExecutorCall = false) 'object' => ['type' => $this->someObjectType], ]; }, - 'interfaces' => function () { + 'interfaces' => function () : array { $this->calls[] = 'SomeObject.interfaces'; return [ @@ -288,9 +289,9 @@ public function loadType($name, $isExecutorCall = false) }, ]); case 'OtherObject': - return $this->otherObjectType ?: $this->otherObjectType = new ObjectType([ + return $this->otherObjectType ?? $this->otherObjectType = new ObjectType([ 'name' => 'OtherObject', - 'fields' => function () { + 'fields' => function () : array { $this->calls[] = 'OtherObject.fields'; return [ @@ -300,16 +301,16 @@ public function loadType($name, $isExecutorCall = false) }, ]); case 'DeeperObject': - return $this->deeperObjectType ?: $this->deeperObjectType = new ObjectType([ + return $this->deeperObjectType ?? $this->deeperObjectType = new ObjectType([ 'name' => 'DeeperObject', - 'fields' => function () { + 'fields' => function () : array { return [ 'scalar' => ['type' => $this->loadType('SomeScalar')], ]; }, ]); case 'SomeScalar': - return $this->someScalarType ?: $this->someScalarType = new CustomScalarType([ + return $this->someScalarType ?? $this->someScalarType = new CustomScalarType([ 'name' => 'SomeScalar', 'serialize' => static function ($value) { return $value; @@ -317,32 +318,32 @@ public function loadType($name, $isExecutorCall = false) 'parseValue' => static function ($value) { return $value; }, - 'parseLiteral' => static function () { + 'parseLiteral' => static function () : void { }, ]); case 'SomeUnion': - return $this->someUnionType ?: $this->someUnionType = new UnionType([ + return $this->someUnionType ?? $this->someUnionType = new UnionType([ 'name' => 'SomeUnion', 'resolveType' => function () { $this->calls[] = 'SomeUnion.resolveType'; return $this->loadType('DeeperObject'); }, - 'types' => function () { + 'types' => function () : array { $this->calls[] = 'SomeUnion.types'; return [$this->loadType('DeeperObject')]; }, ]); case 'SomeInterface': - return $this->someInterfaceType ?: $this->someInterfaceType = new InterfaceType([ + return $this->someInterfaceType ?? $this->someInterfaceType = new InterfaceType([ 'name' => 'SomeInterface', 'resolveType' => function () { $this->calls[] = 'SomeInterface.resolveType'; return $this->loadType('SomeObject'); }, - 'fields' => function () { + 'fields' => function () : array { $this->calls[] = 'SomeInterface.fields'; return [ @@ -380,7 +381,7 @@ public function testDeepQuery() : void 'OtherObject' => true, ]; - self::assertEquals($expected, $result->toArray(true)); + self::assertEquals($expected, $result->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)); self::assertEquals($expectedLoadedTypes, $this->loadedTypes); $expectedExecutorCalls = [ @@ -428,7 +429,7 @@ public function testResolveUnion() : void 'SomeScalar' => true, ]; - self::assertEquals($expected, $result->toArray(true)); + self::assertEquals($expected, $result->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)); self::assertEquals($expectedLoadedTypes, $this->loadedTypes); $expectedCalls = [ diff --git a/tests/Executor/ExecutorSchemaTest.php b/tests/Executor/ExecutorSchemaTest.php index 4946c593a..4a35291a2 100644 --- a/tests/Executor/ExecutorSchemaTest.php +++ b/tests/Executor/ExecutorSchemaTest.php @@ -16,6 +16,7 @@ class ExecutorSchemaTest extends TestCase { // Execute: Handles execution with a complex schema + /** * @see it('executes using a schema') */ @@ -75,13 +76,13 @@ public function testExecutesUsingASchema() : void 'article' => [ 'type' => $BlogArticle, 'args' => ['id' => ['type' => Type::id()]], - 'resolve' => function ($_, $args) { + 'resolve' => function ($rootValue, $args) { return $this->article($args['id']); }, ], 'feed' => [ 'type' => Type::listOf($BlogArticle), - 'resolve' => function () { + 'resolve' => function () : array { return [ $this->article(1), $this->article(2), @@ -198,7 +199,7 @@ public function testExecutesUsingASchema() : void 'isPublished' => true, 'title' => 'My Article 1', 'body' => 'This is a post', - 'keywords' => ['foo', 'bar', '1', 'true', null], + 'keywords' => ['foo', 'bar', '1', '1', null], ], ], 'meta' => [ 'title' => 'My Article 1 | My Blog' ], @@ -212,7 +213,7 @@ public function testExecutesUsingASchema() : void private function article($id) { $johnSmith = null; - $article = static function ($id) use (&$johnSmith) { + $article = static function ($id) use (&$johnSmith) : array { return [ 'id' => $id, 'isPublished' => 'true', @@ -225,7 +226,7 @@ private function article($id) ]; }; - $getPic = static function ($uid, $width, $height) { + $getPic = static function ($uid, $width, $height) : array { return [ 'url' => sprintf('cdn://%s', $uid), 'width' => $width, @@ -236,7 +237,7 @@ private function article($id) $johnSmith = [ 'id' => 123, 'name' => 'John Smith', - 'pic' => static function ($width, $height) use ($getPic) { + 'pic' => static function ($width, $height) use ($getPic) : array { return $getPic(123, $width, $height); }, 'recentArticle' => $article(1), diff --git a/tests/Executor/ExecutorTest.php b/tests/Executor/ExecutorTest.php index 9f5480f3a..190f05a0e 100644 --- a/tests/Executor/ExecutorTest.php +++ b/tests/Executor/ExecutorTest.php @@ -4,14 +4,19 @@ namespace GraphQL\Tests\Executor; +use ArrayAccess; +use Exception; use GraphQL\Deferred; use GraphQL\Error\Error; use GraphQL\Error\UserError; use GraphQL\Executor\Executor; +use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\Parser; use GraphQL\Tests\Executor\TestClasses\NotSpecial; use GraphQL\Tests\Executor\TestClasses\Special; +use GraphQL\Tests\PHPUnit\ArraySubsetAsserts; use GraphQL\Type\Definition\EnumType; +use GraphQL\Type\Definition\FieldDefinition; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; @@ -20,13 +25,14 @@ use GraphQL\Type\Schema; use PHPUnit\Framework\TestCase; use stdClass; -use function array_keys; use function count; use function json_encode; class ExecutorTest extends TestCase { - public function tearDown() + use ArraySubsetAsserts; + + public function tearDown() : void { Executor::setPromiseAdapter(null); } @@ -41,33 +47,33 @@ public function testExecutesArbitraryCode() : void $deepData = null; $data = null; - $promiseData = static function () use (&$data) { + $promiseData = static function () use (&$data) : Deferred { return new Deferred(static function () use (&$data) { return $data; }); }; $data = [ - 'a' => static function () { + 'a' => static function () : string { return 'Apple'; }, - 'b' => static function () { + 'b' => static function () : string { return 'Banana'; }, - 'c' => static function () { + 'c' => static function () : string { return 'Cookie'; }, - 'd' => static function () { + 'd' => static function () : string { return 'Donut'; }, - 'e' => static function () { + 'e' => static function () : string { return 'Egg'; }, 'f' => 'Fish', - 'pic' => static function ($size = 50) { + 'pic' => static function ($size = 50) : string { return 'Pic of size: ' . $size; }, - 'promise' => static function () use ($promiseData) { + 'promise' => static function () use ($promiseData) : Deferred { return $promiseData(); }, 'deep' => static function () use (&$deepData) { @@ -77,16 +83,16 @@ public function testExecutesArbitraryCode() : void // Required for that & reference above $deepData = [ - 'a' => static function () { + 'a' => static function () : string { return 'Already Been Done'; }, - 'b' => static function () { + 'b' => static function () : string { return 'Boring'; }, - 'c' => static function () { + 'c' => static function () : array { return ['Contrived', null, 'Confusing']; }, - 'deeper' => static function () use (&$data) { + 'deeper' => static function () use (&$data) : array { return [$data, null, $data]; }, ]; @@ -212,25 +218,25 @@ public function testMergesParallelFragments() : void return [ 'a' => [ 'type' => Type::string(), - 'resolve' => static function () { + 'resolve' => static function () : string { return 'Apple'; }, ], 'b' => [ 'type' => Type::string(), - 'resolve' => static function () { + 'resolve' => static function () : string { return 'Banana'; }, ], 'c' => [ 'type' => Type::string(), - 'resolve' => static function () { + 'resolve' => static function () : string { return 'Cherry'; }, ], 'deep' => [ 'type' => $Type, - 'resolve' => static function () { + 'resolve' => static function () : array { return []; }, ], @@ -273,7 +279,7 @@ public function testProvidesInfoAboutCurrentExecutionState() : void 'fields' => [ 'test' => [ 'type' => Type::string(), - 'resolve' => static function ($val, $args, $ctx, $_info) use (&$info) { + 'resolve' => static function ($test, $args, $ctx, $_info) use (&$info) : void { $info = $_info; }, ], @@ -285,16 +291,20 @@ public function testProvidesInfoAboutCurrentExecutionState() : void Executor::execute($schema, $ast, $rootValue, null, ['var' => '123']); + /** @var OperationDefinitionNode $operationDefinition */ + $operationDefinition = $ast->definitions[0]; + self::assertEquals('test', $info->fieldName); self::assertEquals(1, count($info->fieldNodes)); - self::assertSame($ast->definitions[0]->selectionSet->selections[0], $info->fieldNodes[0]); + self::assertSame($operationDefinition->selectionSet->selections[0], $info->fieldNodes[0]); self::assertSame(Type::string(), $info->returnType); self::assertSame($schema->getQueryType(), $info->parentType); self::assertEquals(['result'], $info->path); self::assertSame($schema, $info->schema); self::assertSame($rootValue, $info->rootValue); - self::assertEquals($ast->definitions[0], $info->operation); + self::assertEquals($operationDefinition, $info->operation); self::assertEquals(['var' => '123'], $info->variableValues); + self::assertInstanceOf(FieldDefinition::class, $info->fieldDefinition); } /** @@ -316,7 +326,7 @@ public function testThreadsContextCorrectly() : void 'fields' => [ 'a' => [ 'type' => Type::string(), - 'resolve' => static function ($context) use (&$gotHere) { + 'resolve' => static function ($context) use (&$gotHere) : void { self::assertEquals('thing', $context['contextThing']); $gotHere = true; }, @@ -353,7 +363,7 @@ public function testCorrectlyThreadsArguments() : void 'stringArg' => ['type' => Type::string()], ], 'type' => Type::string(), - 'resolve' => static function ($_, $args) use (&$gotHere) { + 'resolve' => static function ($_, $args) use (&$gotHere) : void { self::assertEquals(123, $args['numArg']); self::assertEquals('foo', $args['stringArg']); $gotHere = true; @@ -388,21 +398,21 @@ public function testNullsOutErrorSubtrees() : void }'; $data = [ - 'sync' => static function () { + 'sync' => static function () : string { return 'sync'; }, - 'syncError' => static function () { + 'syncError' => static function () : void { throw new UserError('Error getting syncError'); }, - 'syncRawError' => static function () { + 'syncRawError' => static function () : void { throw new UserError('Error getting syncRawError'); }, // inherited from JS reference implementation, but make no sense in this PHP impl // leaving it just to simplify migrations from newer js versions - 'syncReturnError' => static function () { + 'syncReturnError' => static function () : UserError { return new UserError('Error getting syncReturnError'); }, - 'syncReturnErrorList' => static function () { + 'syncReturnErrorList' => static function () : array { return [ 'sync0', new UserError('Error getting syncReturnErrorList1'), @@ -410,50 +420,50 @@ public function testNullsOutErrorSubtrees() : void new UserError('Error getting syncReturnErrorList3'), ]; }, - 'async' => static function () { - return new Deferred(static function () { + 'async' => static function () : Deferred { + return new Deferred(static function () : string { return 'async'; }); }, - 'asyncReject' => static function () { - return new Deferred(static function () { + 'asyncReject' => static function () : Deferred { + return new Deferred(static function () : void { throw new UserError('Error getting asyncReject'); }); }, - 'asyncRawReject' => static function () { - return new Deferred(static function () { + 'asyncRawReject' => static function () : Deferred { + return new Deferred(static function () : void { throw new UserError('Error getting asyncRawReject'); }); }, - 'asyncEmptyReject' => static function () { - return new Deferred(static function () { + 'asyncEmptyReject' => static function () : Deferred { + return new Deferred(static function () : void { throw new UserError(); }); }, - 'asyncError' => static function () { - return new Deferred(static function () { + 'asyncError' => static function () : Deferred { + return new Deferred(static function () : void { throw new UserError('Error getting asyncError'); }); }, // inherited from JS reference implementation, but make no sense in this PHP impl // leaving it just to simplify migrations from newer js versions - 'asyncRawError' => static function () { - return new Deferred(static function () { + 'asyncRawError' => static function () : Deferred { + return new Deferred(static function () : void { throw new UserError('Error getting asyncRawError'); }); }, - 'asyncReturnError' => static function () { - return new Deferred(static function () { + 'asyncReturnError' => static function () : Deferred { + return new Deferred(static function () : void { throw new UserError('Error getting asyncReturnError'); }); }, - 'asyncReturnErrorWithExtensions' => static function () { - return new Deferred(static function () { + 'asyncReturnErrorWithExtensions' => static function () : Deferred { + return new Deferred(static function () : void { $error = new Error( 'Error getting asyncReturnErrorWithExtensions', null, null, - null, + [], null, null, ['foo' => 'bar'] @@ -816,23 +826,23 @@ public function testCorrectFieldOrderingDespiteExecutionOrder() : void e }'; $data = [ - 'a' => static function () { + 'a' => static function () : string { return 'a'; }, 'b' => static function () { - return new Deferred(static function () { + return new Deferred(static function () : string { return 'b'; }); }, - 'c' => static function () { + 'c' => static function () : string { return 'c'; }, - 'd' => static function () { - return new Deferred(static function () { + 'd' => static function () : Deferred { + return new Deferred(static function () : string { return 'd'; }); }, - 'e' => static function () { + 'e' => static function () : string { return 'e'; }, ]; @@ -965,7 +975,7 @@ public function testFailsWhenAnIsTypeOfCheckIsNotMet() : void { $SpecialType = new ObjectType([ 'name' => 'SpecialType', - 'isTypeOf' => static function ($obj) { + 'isTypeOf' => static function ($obj) : bool { return $obj instanceof Special; }, 'fields' => [ @@ -1060,7 +1070,7 @@ public function testUsesACustomFieldResolver() : void ]); // For the purposes of test, just return the name of the field! - $customResolver = static function ($source, $args, $context, ResolveInfo $info) { + $customResolver = static function ($source, $args, $context, ResolveInfo $info) : string { return $info->fieldName; }; @@ -1147,7 +1157,7 @@ public function testSerializesToEmptyObjectVsEmptyArray() : void 'fields' => [ 'id' => Type::id(), ], - 'interfaces' => static function () use (&$iface) { + 'interfaces' => static function () use (&$iface) : array { return [$iface]; }, ]); @@ -1157,7 +1167,7 @@ public function testSerializesToEmptyObjectVsEmptyArray() : void 'fields' => [ 'id' => Type::id(), ], - 'interfaces' => static function () use (&$iface) { + 'interfaces' => static function () use (&$iface) : array { return [$iface]; }, ]); @@ -1167,7 +1177,7 @@ public function testSerializesToEmptyObjectVsEmptyArray() : void 'fields' => [ 'id' => Type::id(), ], - 'resolveType' => static function ($v) use ($a, $b) { + 'resolveType' => static function ($v) use ($a, $b) : ObjectType { return $v['type'] === 'A' ? $a : $b; }, ]); @@ -1217,4 +1227,185 @@ public function testSerializesToEmptyObjectVsEmptyArray() : void $result->toArray() ); } + + public function testDefaultResolverGrabsValuesOffOfCommonPhpDataStructures() : void + { + $Array = new ObjectType([ + 'name' => 'Array', + 'fields' => [ + 'set' => Type::int(), + 'unset' => Type::int(), + ], + ]); + + $ArrayAccess = new ObjectType([ + 'name' => 'ArrayAccess', + 'fields' => [ + 'set' => Type::int(), + 'unsetNull' => Type::int(), + 'unsetThrow' => Type::int(), + ], + ]); + + $ObjectField = new ObjectType([ + 'name' => 'ObjectField', + 'fields' => [ + 'set' => Type::int(), + 'unset' => Type::int(), + 'nonExistent' => Type::int(), + ], + ]); + + $ObjectVirtual = new ObjectType([ + 'name' => 'ObjectVirtual', + 'fields' => [ + 'set' => Type::int(), + 'unsetNull' => Type::int(), + 'unsetThrow' => Type::int(), + ], + ]); + + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'array' => [ + 'type' => $Array, + 'resolve' => static function () : array { + return ['set' => 1]; + }, + ], + 'arrayAccess' => [ + 'type' => $ArrayAccess, + 'resolve' => static function () : ArrayAccess { + return new class implements ArrayAccess { + public function offsetExists($offset) + { + switch ($offset) { + case 'set': + return true; + default: + return false; + } + } + + public function offsetGet($offset) + { + switch ($offset) { + case 'set': + return 1; + case 'unsetNull': + return null; + default: + throw new Exception('unsetThrow'); + } + } + + public function offsetSet($offset, $value) + { + } + + public function offsetUnset($offset) + { + } + }; + }, + ], + 'objectField' => [ + 'type' => $ObjectField, + 'resolve' => static function () : stdClass { + return new class extends stdClass { + /** @var int|null */ + public $set = 1; + + /** @var int|null */ + public $unset; + }; + }, + ], + 'objectVirtual' => [ + 'type' => $ObjectVirtual, + 'resolve' => static function () { + return new class { + public function __isset($name) : bool + { + switch ($name) { + case 'set': + return true; + default: + return false; + } + } + + public function __get($name) : ?int + { + switch ($name) { + case 'set': + return 1; + case 'unsetNull': + return null; + default: + throw new Exception('unsetThrow'); + } + } + }; + }, + ], + ], + ]), + ]); + + $query = Parser::parse(' + { + array { + set + unset + } + arrayAccess { + set + unsetNull + unsetThrow + } + objectField { + set + unset + nonExistent + } + objectVirtual { + set + unsetNull + unsetThrow + } + } + '); + + $result = Executor::execute($schema, $query); + + self::assertEquals( + [ + 'data' => [ + 'array' => [ + 'set' => 1, + 'unset' => null, + ], + 'arrayAccess' => [ + 'set' => 1, + 'unsetNull' => null, + 'unsetThrow' => null, + ], + 'objectField' => [ + 'set' => 1, + 'unset' => null, + 'nonExistent' => null, + ], + 'objectVirtual' => [ + 'set' => 1, + 'unsetNull' => null, + 'unsetThrow' => null, + ], + ], + ], + $result->toArray() + ); + } } diff --git a/tests/Executor/LazyInterfaceTest.php b/tests/Executor/LazyInterfaceTest.php index c03d40c5b..7917b483d 100644 --- a/tests/Executor/LazyInterfaceTest.php +++ b/tests/Executor/LazyInterfaceTest.php @@ -2,12 +2,6 @@ declare(strict_types=1); -/** - * @author: Ivo Meißner - * Date: 03.05.16 - * Time: 13:14 - */ - namespace GraphQL\Tests\Executor; use GraphQL\Executor\Executor; @@ -56,7 +50,7 @@ public function testReturnsFragmentsWithLazyCreatedInterface() : void /** * Setup schema */ - protected function setUp() + protected function setUp() : void { $query = new ObjectType([ 'name' => 'query', @@ -64,7 +58,7 @@ protected function setUp() return [ 'lazyInterface' => [ 'type' => $this->getLazyInterfaceType(), - 'resolve' => static function () { + 'resolve' => static function () : array { return []; }, ], @@ -88,7 +82,7 @@ protected function getLazyInterfaceType() 'fields' => [ 'a' => Type::string(), ], - 'resolveType' => function () { + 'resolveType' => function () : ObjectType { return $this->getTestObjectType(); }, ]); @@ -110,7 +104,7 @@ protected function getTestObjectType() 'fields' => [ 'name' => [ 'type' => Type::string(), - 'resolve' => static function () { + 'resolve' => static function () : string { return 'testname'; }, ], diff --git a/tests/Executor/ListsTest.php b/tests/Executor/ListsTest.php index 1ebe5962b..b83bf1b2d 100644 --- a/tests/Executor/ListsTest.php +++ b/tests/Executor/ListsTest.php @@ -5,9 +5,11 @@ namespace GraphQL\Tests\Executor; use GraphQL\Deferred; +use GraphQL\Error\DebugFlag; use GraphQL\Error\UserError; use GraphQL\Executor\Executor; use GraphQL\Language\Parser; +use GraphQL\Tests\PHPUnit\ArraySubsetAsserts; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; @@ -15,7 +17,10 @@ class ListsTest extends TestCase { + use ArraySubsetAsserts; + // Describe: Execute: Handles list nullability + /** * [T] */ @@ -46,19 +51,19 @@ private function checkHandlesNullableLists($testData, $expected) $this->check($testType, $testData, $expected); } - private function check($testType, $testData, $expected, $debug = false) + private function check($testType, $testData, $expected, int $debug = DebugFlag::NONE) { $data = ['test' => $testData]; $dataType = null; $dataType = new ObjectType([ 'name' => 'DataType', - 'fields' => static function () use (&$testType, &$dataType, $data) { + 'fields' => static function () use (&$testType, &$dataType, $data) : array { return [ 'test' => ['type' => $testType], 'nest' => [ 'type' => $dataType, - 'resolve' => static function () use ($data) { + 'resolve' => static function () use ($data) : array { return $data; }, ], @@ -81,7 +86,7 @@ public function testHandlesNullableListsWithPromiseArray() : void { // Contains values $this->checkHandlesNullableLists( - new Deferred(static function () { + new Deferred(static function () : array { return [1, 2]; }), ['data' => ['nest' => ['test' => [1, 2]]]] @@ -89,7 +94,7 @@ public function testHandlesNullableListsWithPromiseArray() : void // Contains null $this->checkHandlesNullableLists( - new Deferred(static function () { + new Deferred(static function () : array { return [1, null, 2]; }), ['data' => ['nest' => ['test' => [1, null, 2]]]] @@ -105,8 +110,8 @@ public function testHandlesNullableListsWithPromiseArray() : void // Rejected $this->checkHandlesNullableLists( - static function () { - return new Deferred(static function () { + static function () : Deferred { + return new Deferred(static function () : void { throw new UserError('bad'); }); }, @@ -131,10 +136,10 @@ public function testHandlesNullableListsWithArrayPromise() : void // Contains values $this->checkHandlesNullableLists( [ - new Deferred(static function () { + new Deferred(static function () : int { return 1; }), - new Deferred(static function () { + new Deferred(static function () : int { return 2; }), ], @@ -144,13 +149,13 @@ public function testHandlesNullableListsWithArrayPromise() : void // Contains null $this->checkHandlesNullableLists( [ - new Deferred(static function () { + new Deferred(static function () : int { return 1; }), new Deferred(static function () { return null; }), - new Deferred(static function () { + new Deferred(static function () : int { return 2; }), ], @@ -169,13 +174,13 @@ public function testHandlesNullableListsWithArrayPromise() : void $this->checkHandlesNullableLists( static function () { return [ - new Deferred(static function () { + new Deferred(static function () : int { return 1; }), - new Deferred(static function () { + new Deferred(static function () : void { throw new UserError('bad'); }), - new Deferred(static function () { + new Deferred(static function () : int { return 2; }), ]; @@ -217,16 +222,16 @@ public function testHandlesNonNullableListsWithArray() : void 'data' => ['nest' => null], 'errors' => [ [ - 'debugMessage' => 'Cannot return null for non-nullable field DataType.test.', + 'debugMessage' => 'Cannot return null for non-nullable field "DataType.test".', 'locations' => [['line' => 1, 'column' => 10]], ], ], ], - true + DebugFlag::INCLUDE_DEBUG_MESSAGE ); } - private function checkHandlesNonNullableLists($testData, $expected, $debug = false) + private function checkHandlesNonNullableLists($testData, $expected, int $debug = DebugFlag::NONE) { $testType = Type::nonNull(Type::listOf(Type::int())); $this->check($testType, $testData, $expected, $debug); @@ -239,7 +244,7 @@ public function testHandlesNonNullableListsWithPromiseArray() : void { // Contains values $this->checkHandlesNonNullableLists( - new Deferred(static function () { + new Deferred(static function () : array { return [1, 2]; }), ['data' => ['nest' => ['test' => [1, 2]]]] @@ -247,7 +252,7 @@ public function testHandlesNonNullableListsWithPromiseArray() : void // Contains null $this->checkHandlesNonNullableLists( - new Deferred(static function () { + new Deferred(static function () : array { return [1, null, 2]; }), ['data' => ['nest' => ['test' => [1, null, 2]]]] @@ -260,18 +265,18 @@ public function testHandlesNonNullableListsWithPromiseArray() : void 'data' => ['nest' => null], 'errors' => [ [ - 'debugMessage' => 'Cannot return null for non-nullable field DataType.test.', + 'debugMessage' => 'Cannot return null for non-nullable field "DataType.test".', 'locations' => [['line' => 1, 'column' => 10]], ], ], ], - true + DebugFlag::INCLUDE_DEBUG_MESSAGE ); // Rejected $this->checkHandlesNonNullableLists( - static function () { - return new Deferred(static function () { + static function () : Deferred { + return new Deferred(static function () : void { throw new UserError('bad'); }); }, @@ -296,10 +301,10 @@ public function testHandlesNonNullableListsWithArrayPromise() : void // Contains values $this->checkHandlesNonNullableLists( [ - new Deferred(static function () { + new Deferred(static function () : int { return 1; }), - new Deferred(static function () { + new Deferred(static function () : int { return 2; }), ], @@ -309,13 +314,13 @@ public function testHandlesNonNullableListsWithArrayPromise() : void // Contains null $this->checkHandlesNonNullableLists( [ - new Deferred(static function () { + new Deferred(static function () : int { return 1; }), new Deferred(static function () { return null; }), - new Deferred(static function () { + new Deferred(static function () : int { return 2; }), ], @@ -326,13 +331,13 @@ public function testHandlesNonNullableListsWithArrayPromise() : void $this->checkHandlesNonNullableLists( static function () { return [ - new Deferred(static function () { + new Deferred(static function () : int { return 1; }), - new Deferred(static function () { + new Deferred(static function () : void { throw new UserError('bad'); }), - new Deferred(static function () { + new Deferred(static function () : int { return 2; }), ]; @@ -368,12 +373,12 @@ public function testHandlesListOfNonNullsWithArray() : void 'data' => ['nest' => ['test' => null]], 'errors' => [ [ - 'debugMessage' => 'Cannot return null for non-nullable field DataType.test.', + 'debugMessage' => 'Cannot return null for non-nullable field "DataType.test".', 'locations' => [['line' => 1, 'column' => 10]], ], ], ], - true + DebugFlag::INCLUDE_DEBUG_MESSAGE ); // Returns null @@ -383,7 +388,7 @@ public function testHandlesListOfNonNullsWithArray() : void ); } - private function checkHandlesListOfNonNulls($testData, $expected, $debug = false) + private function checkHandlesListOfNonNulls($testData, $expected, int $debug = DebugFlag::NONE) { $testType = Type::listOf(Type::nonNull(Type::int())); $this->check($testType, $testData, $expected, $debug); @@ -396,7 +401,7 @@ public function testHandlesListOfNonNullsWithPromiseArray() : void { // Contains values $this->checkHandlesListOfNonNulls( - new Deferred(static function () { + new Deferred(static function () : array { return [1, 2]; }), ['data' => ['nest' => ['test' => [1, 2]]]] @@ -404,19 +409,19 @@ public function testHandlesListOfNonNullsWithPromiseArray() : void // Contains null $this->checkHandlesListOfNonNulls( - new Deferred(static function () { + new Deferred(static function () : array { return [1, null, 2]; }), [ 'data' => ['nest' => ['test' => null]], 'errors' => [ [ - 'debugMessage' => 'Cannot return null for non-nullable field DataType.test.', + 'debugMessage' => 'Cannot return null for non-nullable field "DataType.test".', 'locations' => [['line' => 1, 'column' => 10]], ], ], ], - true + DebugFlag::INCLUDE_DEBUG_MESSAGE ); // Returns null @@ -429,8 +434,8 @@ public function testHandlesListOfNonNullsWithPromiseArray() : void // Rejected $this->checkHandlesListOfNonNulls( - static function () { - return new Deferred(static function () { + static function () : Deferred { + return new Deferred(static function () : void { throw new UserError('bad'); }); }, @@ -455,10 +460,10 @@ public function testHandlesListOfNonNullsWithArrayPromise() : void // Contains values $this->checkHandlesListOfNonNulls( [ - new Deferred(static function () { + new Deferred(static function () : int { return 1; }), - new Deferred(static function () { + new Deferred(static function () : int { return 2; }), ], @@ -468,13 +473,13 @@ public function testHandlesListOfNonNullsWithArrayPromise() : void // Contains null $this->checkHandlesListOfNonNulls( [ - new Deferred(static function () { + new Deferred(static function () : int { return 1; }), new Deferred(static function () { return null; }), - new Deferred(static function () { + new Deferred(static function () : int { return 2; }), ], @@ -485,13 +490,13 @@ public function testHandlesListOfNonNullsWithArrayPromise() : void $this->checkHandlesListOfNonNulls( static function () { return [ - new Deferred(static function () { + new Deferred(static function () : int { return 1; }), - new Deferred(static function () { + new Deferred(static function () : void { throw new UserError('bad'); }), - new Deferred(static function () { + new Deferred(static function () : int { return 2; }), ]; @@ -527,12 +532,12 @@ public function testHandlesNonNullListOfNonNullsWithArray() : void 'data' => ['nest' => null], 'errors' => [ [ - 'debugMessage' => 'Cannot return null for non-nullable field DataType.test.', + 'debugMessage' => 'Cannot return null for non-nullable field "DataType.test".', 'locations' => [['line' => 1, 'column' => 10]], ], ], ], - true + DebugFlag::INCLUDE_DEBUG_MESSAGE ); // Returns null @@ -542,16 +547,16 @@ public function testHandlesNonNullListOfNonNullsWithArray() : void 'data' => ['nest' => null], 'errors' => [ [ - 'debugMessage' => 'Cannot return null for non-nullable field DataType.test.', + 'debugMessage' => 'Cannot return null for non-nullable field "DataType.test".', 'locations' => [['line' => 1, 'column' => 10]], ], ], ], - true + DebugFlag::INCLUDE_DEBUG_MESSAGE ); } - public function checkHandlesNonNullListOfNonNulls($testData, $expected, $debug = false) + public function checkHandlesNonNullListOfNonNulls($testData, $expected, int $debug = DebugFlag::NONE) { $testType = Type::nonNull(Type::listOf(Type::nonNull(Type::int()))); $this->check($testType, $testData, $expected, $debug); @@ -564,7 +569,7 @@ public function testHandlesNonNullListOfNonNullsWithPromiseArray() : void { // Contains values $this->checkHandlesNonNullListOfNonNulls( - new Deferred(static function () { + new Deferred(static function () : array { return [1, 2]; }), ['data' => ['nest' => ['test' => [1, 2]]]] @@ -572,19 +577,19 @@ public function testHandlesNonNullListOfNonNullsWithPromiseArray() : void // Contains null $this->checkHandlesNonNullListOfNonNulls( - new Deferred(static function () { + new Deferred(static function () : array { return [1, null, 2]; }), [ 'data' => ['nest' => null], 'errors' => [ [ - 'debugMessage' => 'Cannot return null for non-nullable field DataType.test.', + 'debugMessage' => 'Cannot return null for non-nullable field "DataType.test".', 'locations' => [['line' => 1, 'column' => 10]], ], ], ], - true + DebugFlag::INCLUDE_DEBUG_MESSAGE ); // Returns null @@ -596,18 +601,18 @@ public function testHandlesNonNullListOfNonNullsWithPromiseArray() : void 'data' => ['nest' => null], 'errors' => [ [ - 'debugMessage' => 'Cannot return null for non-nullable field DataType.test.', + 'debugMessage' => 'Cannot return null for non-nullable field "DataType.test".', 'locations' => [['line' => 1, 'column' => 10]], ], ], ], - true + DebugFlag::INCLUDE_DEBUG_MESSAGE ); // Rejected $this->checkHandlesNonNullListOfNonNulls( - static function () { - return new Deferred(static function () { + static function () : Deferred { + return new Deferred(static function () : void { throw new UserError('bad'); }); }, @@ -632,10 +637,10 @@ public function testHandlesNonNullListOfNonNullsWithArrayPromise() : void // Contains values $this->checkHandlesNonNullListOfNonNulls( [ - new Deferred(static function () { + new Deferred(static function () : int { return 1; }), - new Deferred(static function () { + new Deferred(static function () : int { return 2; }), @@ -646,13 +651,13 @@ public function testHandlesNonNullListOfNonNullsWithArrayPromise() : void // Contains null $this->checkHandlesNonNullListOfNonNulls( [ - new Deferred(static function () { + new Deferred(static function () : int { return 1; }), new Deferred(static function () { return null; }), - new Deferred(static function () { + new Deferred(static function () : int { return 2; }), ], @@ -660,25 +665,25 @@ public function testHandlesNonNullListOfNonNullsWithArrayPromise() : void 'data' => ['nest' => null], 'errors' => [ [ - 'debugMessage' => 'Cannot return null for non-nullable field DataType.test.', + 'debugMessage' => 'Cannot return null for non-nullable field "DataType.test".', 'locations' => [['line' => 1, 'column' => 10]], ], ], ], - true + DebugFlag::INCLUDE_DEBUG_MESSAGE ); // Contains reject $this->checkHandlesNonNullListOfNonNulls( static function () { return [ - new Deferred(static function () { + new Deferred(static function () : int { return 1; }), - new Deferred(static function () { + new Deferred(static function () : void { throw new UserError('bad'); }), - new Deferred(static function () { + new Deferred(static function () : int { return 2; }), ]; diff --git a/tests/Executor/MutationsTest.php b/tests/Executor/MutationsTest.php index e511b7055..923267b3b 100644 --- a/tests/Executor/MutationsTest.php +++ b/tests/Executor/MutationsTest.php @@ -4,9 +4,13 @@ namespace GraphQL\Tests\Executor; +use GraphQL\Deferred; +use GraphQL\Error\DebugFlag; use GraphQL\Executor\Executor; use GraphQL\Language\Parser; +use GraphQL\Tests\Executor\TestClasses\NumberHolder; use GraphQL\Tests\Executor\TestClasses\Root; +use GraphQL\Tests\PHPUnit\ArraySubsetAsserts; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; @@ -14,7 +18,10 @@ class MutationsTest extends TestCase { + use ArraySubsetAsserts; + // Execute: Handles mutation execution ordering + /** * @see it('evaluates mutations serially') */ @@ -72,28 +79,28 @@ private function schema() : Schema 'immediatelyChangeTheNumber' => [ 'type' => $numberHolderType, 'args' => ['newNumber' => ['type' => Type::int()]], - 'resolve' => static function (Root $obj, $args) { + 'resolve' => static function (Root $obj, $args) : NumberHolder { return $obj->immediatelyChangeTheNumber($args['newNumber']); }, ], 'promiseToChangeTheNumber' => [ 'type' => $numberHolderType, 'args' => ['newNumber' => ['type' => Type::int()]], - 'resolve' => static function (Root $obj, $args) { + 'resolve' => static function (Root $obj, $args) : Deferred { return $obj->promiseToChangeTheNumber($args['newNumber']); }, ], 'failToChangeTheNumber' => [ 'type' => $numberHolderType, 'args' => ['newNumber' => ['type' => Type::int()]], - 'resolve' => static function (Root $obj, $args) { + 'resolve' => static function (Root $obj, $args) : void { $obj->failToChangeTheNumber(); }, ], 'promiseAndFailToChangeTheNumber' => [ 'type' => $numberHolderType, 'args' => ['newNumber' => ['type' => Type::int()]], - 'resolve' => static function (Root $obj, $args) { + 'resolve' => static function (Root $obj, $args) : Deferred { return $obj->promiseAndFailToChangeTheNumber(); }, ], @@ -150,6 +157,6 @@ public function testEvaluatesMutationsCorrectlyInThePresenseOfAFailedMutation() ], ], ]; - self::assertArraySubset($expected, $mutationResult->toArray(true)); + self::assertArraySubset($expected, $mutationResult->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)); } } diff --git a/tests/Executor/NonNullTest.php b/tests/Executor/NonNullTest.php index 86c7084d5..9b289b2d6 100644 --- a/tests/Executor/NonNullTest.php +++ b/tests/Executor/NonNullTest.php @@ -6,21 +6,26 @@ use Exception; use GraphQL\Deferred; +use GraphQL\Error\DebugFlag; use GraphQL\Error\FormattedError; use GraphQL\Error\UserError; use GraphQL\Executor\Executor; use GraphQL\Language\Parser; use GraphQL\Language\SourceLocation; +use GraphQL\Tests\PHPUnit\ArraySubsetAsserts; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; use function count; +use function is_string; use function json_encode; class NonNullTest extends TestCase { + use ArraySubsetAsserts; + /** @var Exception */ public $syncError; @@ -42,7 +47,10 @@ class NonNullTest extends TestCase /** @var Schema */ public $schema; - public function setUp() + /** @var Schema */ + public $schemaWithNonNullArg; + + public function setUp() : void { $this->syncError = new UserError('sync'); $this->syncNonNullError = new UserError('syncNonNull'); @@ -50,35 +58,35 @@ public function setUp() $this->promiseNonNullError = new UserError('promiseNonNull'); $this->throwingData = [ - 'sync' => function () { + 'sync' => function () : void { throw $this->syncError; }, - 'syncNonNull' => function () { + 'syncNonNull' => function () : void { throw $this->syncNonNullError; }, - 'promise' => function () { - return new Deferred(function () { + 'promise' => function () : Deferred { + return new Deferred(function () : void { throw $this->promiseError; }); }, - 'promiseNonNull' => function () { - return new Deferred(function () { + 'promiseNonNull' => function () : Deferred { + return new Deferred(function () : void { throw $this->promiseNonNullError; }); }, - 'syncNest' => function () { + 'syncNest' => function () : array { return $this->throwingData; }, - 'syncNonNullNest' => function () { + 'syncNonNullNest' => function () : array { return $this->throwingData; }, - 'promiseNest' => function () { - return new Deferred(function () { + 'promiseNest' => function () : Deferred { + return new Deferred(function () : array { return $this->throwingData; }); }, - 'promiseNonNullNest' => function () { - return new Deferred(function () { + 'promiseNonNullNest' => function () : Deferred { + return new Deferred(function () : array { return $this->throwingData; }); }, @@ -91,29 +99,29 @@ public function setUp() 'syncNonNull' => static function () { return null; }, - 'promise' => static function () { + 'promise' => static function () : Deferred { return new Deferred(static function () { return null; }); }, - 'promiseNonNull' => static function () { + 'promiseNonNull' => static function () : Deferred { return new Deferred(static function () { return null; }); }, - 'syncNest' => function () { + 'syncNest' => function () : array { return $this->nullingData; }, - 'syncNonNullNest' => function () { + 'syncNonNullNest' => function () : array { return $this->nullingData; }, - 'promiseNest' => function () { - return new Deferred(function () { + 'promiseNest' => function () : Deferred { + return new Deferred(function () : array { return $this->nullingData; }); }, - 'promiseNonNullNest' => function () { - return new Deferred(function () { + 'promiseNonNullNest' => function () : Deferred { + return new Deferred(function () : array { return $this->nullingData; }); }, @@ -121,7 +129,7 @@ public function setUp() $dataType = new ObjectType([ 'name' => 'DataType', - 'fields' => static function () use (&$dataType) { + 'fields' => static function () use (&$dataType) : array { return [ 'sync' => ['type' => Type::string()], 'syncNonNull' => ['type' => Type::nonNull(Type::string())], @@ -136,6 +144,29 @@ public function setUp() ]); $this->schema = new Schema(['query' => $dataType]); + + $this->schemaWithNonNullArg = new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'withNonNullArg' => [ + 'type' => Type::string(), + 'args' => [ + 'cannotBeNull' => [ + 'type' => Type::nonNull(Type::string()), + ], + ], + 'resolve' => static function ($value, $args) : ?string { + if (is_string($args['cannotBeNull'])) { + return 'Passed: ' . $args['cannotBeNull']; + } + + return null; + }, + ], + ], + ]), + ]); } // Execute: handles non-nullable types @@ -522,14 +553,14 @@ public function testNullsASynchronouslyReturnedObjectThatContainsANonNullableFie 'data' => ['syncNest' => null], 'errors' => [ [ - 'debugMessage' => 'Cannot return null for non-nullable field DataType.syncNonNull.', + 'debugMessage' => 'Cannot return null for non-nullable field "DataType.syncNonNull".', 'locations' => [['line' => 4, 'column' => 11]], ], ], ]; self::assertArraySubset( $expected, - Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray(true) + Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE) ); } @@ -549,7 +580,7 @@ public function testNullsASynchronouslyReturnedObjectThatContainsANonNullableFie 'data' => ['syncNest' => null], 'errors' => [ [ - 'debugMessage' => 'Cannot return null for non-nullable field DataType.promiseNonNull.', + 'debugMessage' => 'Cannot return null for non-nullable field "DataType.promiseNonNull".', 'locations' => [['line' => 4, 'column' => 11]], ], ], @@ -557,7 +588,7 @@ public function testNullsASynchronouslyReturnedObjectThatContainsANonNullableFie self::assertArraySubset( $expected, - Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray(true) + Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE) ); } @@ -577,7 +608,7 @@ public function testNullsAnObjectReturnedInAPromiseThatContainsANonNullableField 'data' => ['promiseNest' => null], 'errors' => [ [ - 'debugMessage' => 'Cannot return null for non-nullable field DataType.syncNonNull.', + 'debugMessage' => 'Cannot return null for non-nullable field "DataType.syncNonNull".', 'locations' => [['line' => 4, 'column' => 11]], ], ], @@ -585,7 +616,7 @@ public function testNullsAnObjectReturnedInAPromiseThatContainsANonNullableField self::assertArraySubset( $expected, - Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray(true) + Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE) ); } @@ -605,7 +636,7 @@ public function testNullsAnObjectReturnedInAPromiseThatContainsANonNullableField 'data' => ['promiseNest' => null], 'errors' => [ [ - 'debugMessage' => 'Cannot return null for non-nullable field DataType.promiseNonNull.', + 'debugMessage' => 'Cannot return null for non-nullable field "DataType.promiseNonNull".', 'locations' => [['line' => 4, 'column' => 11]], ], ], @@ -613,7 +644,7 @@ public function testNullsAnObjectReturnedInAPromiseThatContainsANonNullableField self::assertArraySubset( $expected, - Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray(true) + Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE) ); } @@ -683,6 +714,9 @@ public function testNullsAComplexTreeOfNullableFieldsThatReturnNull() : void self::assertEquals($expected, $actual); } + /** + * @see it('nulls the first nullable object after a field in a long chain of non-null fields') + */ public function testNullsTheFirstNullableObjectAfterAFieldReturnsNullInALongChainOfFieldsThatAreNonNull() : void { $doc = ' @@ -744,21 +778,21 @@ public function testNullsTheFirstNullableObjectAfterAFieldReturnsNullInALongChai 'anotherPromiseNest' => null, ], 'errors' => [ - ['debugMessage' => 'Cannot return null for non-nullable field DataType.syncNonNull.', 'locations' => [['line' => 8, 'column' => 19]]], - ['debugMessage' => 'Cannot return null for non-nullable field DataType.syncNonNull.', 'locations' => [['line' => 19, 'column' => 19]]], - ['debugMessage' => 'Cannot return null for non-nullable field DataType.promiseNonNull.', 'locations' => [['line' => 30, 'column' => 19]]], - ['debugMessage' => 'Cannot return null for non-nullable field DataType.promiseNonNull.', 'locations' => [['line' => 41, 'column' => 19]]], + ['debugMessage' => 'Cannot return null for non-nullable field "DataType.syncNonNull".', 'locations' => [['line' => 8, 'column' => 19]]], + ['debugMessage' => 'Cannot return null for non-nullable field "DataType.syncNonNull".', 'locations' => [['line' => 19, 'column' => 19]]], + ['debugMessage' => 'Cannot return null for non-nullable field "DataType.promiseNonNull".', 'locations' => [['line' => 30, 'column' => 19]]], + ['debugMessage' => 'Cannot return null for non-nullable field "DataType.promiseNonNull".', 'locations' => [['line' => 41, 'column' => 19]]], ], ]; self::assertArraySubset( $expected, - Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray(true) + Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE) ); } /** - * @see it('nulls the top level if sync non-nullable field throws') + * @see it('nulls the top level if non-nullable field') */ public function testNullsTheTopLevelIfSyncNonNullableFieldThrows() : void { @@ -775,6 +809,194 @@ public function testNullsTheTopLevelIfSyncNonNullableFieldThrows() : void self::assertArraySubset($expected, $actual); } + /** + * @see describe('Handles non-null argument') + * @see it('succeeds when passed non-null literal value') + */ + public function succeedsWhenPassedNonNullLiteralValue() : void + { + $result = Executor::execute( + $this->schemaWithNonNullArg, + Parser::parse(' + query { + withNonNullArg (cannotBeNull: "literal value") + } + ') + ); + + $expected = ['data' => ['withNonNullArg' => 'Passed: literal value']]; + self::assertEquals($expected, $result->toArray()); + } + + /** + * @see it('succeeds when passed non-null variable value') + */ + public function succeedsWhenPassedNonNullVariableValue() + { + $result = Executor::execute( + $this->schemaWithNonNullArg, + Parser::parse(' + query ($testVar: String!) { + withNonNullArg (cannotBeNull: $testVar) + } + '), + null, + null, + ['testVar' => 'variable value'] + ); + + $expected = ['data' => ['withNonNullArg' => 'Passed: variable value']]; + self::assertEquals($expected, $result->toArray()); + } + + /** + * @see it('succeeds when missing variable has default value') + */ + public function testSucceedsWhenMissingVariableHasDefaultValue() + { + $result = Executor::execute( + $this->schemaWithNonNullArg, + Parser::parse(' + query ($testVar: String = "default value") { + withNonNullArg (cannotBeNull: $testVar) + } + '), + null, + null, + [] // Intentionally missing variable + ); + + $expected = ['data' => ['withNonNullArg' => 'Passed: default value']]; + self::assertEquals($expected, $result->toArray()); + } + + /** + * @see it('field error when missing non-null arg') + */ + public function testFieldErrorWhenMissingNonNullArg() + { + // Note: validation should identify this issue first (missing args rule) + // however execution should still protect against this. + $result = Executor::execute( + $this->schemaWithNonNullArg, + Parser::parse(' + query { + withNonNullArg + } + ') + ); + + $expected = [ + 'data' => ['withNonNullArg' => null], + 'errors' => [ + [ + 'message' => 'Argument "cannotBeNull" of required type "String!" was not provided.', + 'locations' => [['line' => 3, 'column' => 13]], + 'path' => ['withNonNullArg'], + 'extensions' => ['category' => 'graphql'], + ], + ], + ]; + self::assertEquals($expected, $result->toArray()); + } + + /** + * @see it('field error when non-null arg provided null') + */ + public function testFieldErrorWhenNonNullArgProvidedNull() + { + // Note: validation should identify this issue first (values of correct + // type rule) however execution should still protect against this. + $result = Executor::execute( + $this->schemaWithNonNullArg, + Parser::parse(' + query { + withNonNullArg(cannotBeNull: null) + } + ') + ); + + $expected = [ + 'data' => ['withNonNullArg' => null], + 'errors' => [ + [ + 'message' => 'Argument "cannotBeNull" of non-null type "String!" must not be null.', + 'locations' => [['line' => 3, 'column' => 13]], + 'path' => ['withNonNullArg'], + 'extensions' => ['category' => 'graphql'], + ], + ], + ]; + self::assertEquals($expected, $result->toArray()); + } + + /** + * @see it('field error when non-null arg not provided variable value') + */ + public function testFieldErrorWhenNonNullArgNotProvidedVariableValue() : void + { + // Note: validation should identify this issue first (variables in allowed + // position rule) however execution should still protect against this. + $result = Executor::execute( + $this->schemaWithNonNullArg, + Parser::parse(' + query ($testVar: String) { + withNonNullArg(cannotBeNull: $testVar) + } + '), + null, + null, + [] // Intentionally missing variable + ); + + $expected = [ + 'data' => ['withNonNullArg' => null], + 'errors' => [ + [ + 'message' => 'Argument "cannotBeNull" of required type "String!" was ' . + 'provided the variable "$testVar" which was not provided a ' . + 'runtime value.', + 'locations' => [['line' => 3, 'column' => 42]], + 'path' => ['withNonNullArg'], + 'extensions' => ['category' => 'graphql'], + ], + ], + ]; + self::assertEquals($expected, $result->toArray()); + } + + /** + * @see it('field error when non-null arg provided variable with explicit null value') + */ + public function testFieldErrorWhenNonNullArgProvidedVariableWithExplicitNullValue() + { + $result = Executor::execute( + $this->schemaWithNonNullArg, + Parser::parse(' + query ($testVar: String = "default value") { + withNonNullArg (cannotBeNull: $testVar) + } + '), + null, + null, + ['testVar' => null] + ); + + $expected = [ + 'data' => ['withNonNullArg' => null], + 'errors' => [ + [ + 'message' => 'Argument "cannotBeNull" of non-null type "String!" must not be null.', + 'locations' => [['line' => 3, 'column' => 13]], + 'path' => ['withNonNullArg'], + 'extensions' => ['category' => 'graphql'], + ], + ], + ]; + + self::assertEquals($expected, $result->toArray()); + } + public function testNullsTheTopLevelIfAsyncNonNullableFieldErrors() : void { $doc = ' @@ -805,14 +1027,14 @@ public function testNullsTheTopLevelIfSyncNonNullableFieldReturnsNull() : void $expected = [ 'errors' => [ [ - 'debugMessage' => 'Cannot return null for non-nullable field DataType.syncNonNull.', + 'debugMessage' => 'Cannot return null for non-nullable field "DataType.syncNonNull".', 'locations' => [['line' => 2, 'column' => 17]], ], ], ]; self::assertArraySubset( $expected, - Executor::execute($this->schema, Parser::parse($doc), $this->nullingData)->toArray(true) + Executor::execute($this->schema, Parser::parse($doc), $this->nullingData)->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE) ); } @@ -827,7 +1049,7 @@ public function testNullsTheTopLevelIfAsyncNonNullableFieldResolvesNull() : void $expected = [ 'errors' => [ [ - 'debugMessage' => 'Cannot return null for non-nullable field DataType.promiseNonNull.', + 'debugMessage' => 'Cannot return null for non-nullable field "DataType.promiseNonNull".', 'locations' => [['line' => 2, 'column' => 17]], ], ], @@ -835,7 +1057,7 @@ public function testNullsTheTopLevelIfAsyncNonNullableFieldResolvesNull() : void self::assertArraySubset( $expected, - Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray(true) + Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE) ); } } diff --git a/tests/Executor/Promise/AmpPromiseAdapterTest.php b/tests/Executor/Promise/AmpPromiseAdapterTest.php new file mode 100644 index 000000000..09e2090be --- /dev/null +++ b/tests/Executor/Promise/AmpPromiseAdapterTest.php @@ -0,0 +1,183 @@ +isThenable(call(static function () : Generator { + yield from []; + })) + ); + self::assertTrue($ampAdapter->isThenable(new Success())); + self::assertTrue($ampAdapter->isThenable(new Failure(new Exception()))); + self::assertTrue($ampAdapter->isThenable(new Delayed(0))); + self::assertTrue( + $ampAdapter->isThenable(new LazyPromise(static function () : void { + })) + ); + self::assertFalse($ampAdapter->isThenable(false)); + self::assertFalse($ampAdapter->isThenable(true)); + self::assertFalse($ampAdapter->isThenable(1)); + self::assertFalse($ampAdapter->isThenable(0)); + self::assertFalse($ampAdapter->isThenable('test')); + self::assertFalse($ampAdapter->isThenable('')); + self::assertFalse($ampAdapter->isThenable([])); + self::assertFalse($ampAdapter->isThenable(new stdClass())); + } + + public function testConvertsReactPromisesToGraphQlOnes() : void + { + $ampAdapter = new AmpPromiseAdapter(); + $ampPromise = new Success(1); + + $promise = $ampAdapter->convertThenable($ampPromise); + + self::assertInstanceOf('GraphQL\Executor\Promise\Promise', $promise); + self::assertInstanceOf(Success::class, $promise->adoptedPromise); + } + + public function testThen() : void + { + $ampAdapter = new AmpPromiseAdapter(); + $ampPromise = new Success(1); + $promise = $ampAdapter->convertThenable($ampPromise); + + $result = null; + + $resultPromise = $ampAdapter->then( + $promise, + static function ($value) use (&$result) : void { + $result = $value; + } + ); + + self::assertSame(1, $result); + self::assertInstanceOf('GraphQL\Executor\Promise\Promise', $resultPromise); + self::assertInstanceOf(Promise::class, $resultPromise->adoptedPromise); + } + + public function testCreate() : void + { + $ampAdapter = new AmpPromiseAdapter(); + $resolvedPromise = $ampAdapter->create(static function ($resolve) : void { + $resolve(1); + }); + + self::assertInstanceOf('GraphQL\Executor\Promise\Promise', $resolvedPromise); + self::assertInstanceOf(Promise::class, $resolvedPromise->adoptedPromise); + + $result = null; + + $resolvedPromise->then(static function ($value) use (&$result) : void { + $result = $value; + }); + + self::assertSame(1, $result); + } + + public function testCreateFulfilled() : void + { + $ampAdapter = new AmpPromiseAdapter(); + $fulfilledPromise = $ampAdapter->createFulfilled(1); + + self::assertInstanceOf('GraphQL\Executor\Promise\Promise', $fulfilledPromise); + self::assertInstanceOf(Success::class, $fulfilledPromise->adoptedPromise); + + $result = null; + + $fulfilledPromise->then(static function ($value) use (&$result) : void { + $result = $value; + }); + + self::assertSame(1, $result); + } + + public function testCreateRejected() : void + { + $ampAdapter = new AmpPromiseAdapter(); + $rejectedPromise = $ampAdapter->createRejected(new Exception('I am a bad promise')); + + self::assertInstanceOf('GraphQL\Executor\Promise\Promise', $rejectedPromise); + self::assertInstanceOf(Failure::class, $rejectedPromise->adoptedPromise); + + $exception = null; + + $rejectedPromise->then( + null, + static function ($error) use (&$exception) : void { + $exception = $error; + } + ); + + self::assertInstanceOf('\Exception', $exception); + self::assertEquals('I am a bad promise', $exception->getMessage()); + } + + public function testAll() : void + { + $ampAdapter = new AmpPromiseAdapter(); + $promises = [new Success(1), new Success(2), new Success(3)]; + + $allPromise = $ampAdapter->all($promises); + + self::assertInstanceOf('GraphQL\Executor\Promise\Promise', $allPromise); + self::assertInstanceOf(Promise::class, $allPromise->adoptedPromise); + + $result = null; + + $allPromise->then(static function ($values) use (&$result) : void { + $result = $values; + }); + + self::assertSame([1, 2, 3], $result); + } + + public function testAllShouldPreserveTheOrderOfTheArrayWhenResolvingAsyncPromises() : void + { + $ampAdapter = new AmpPromiseAdapter(); + $deferred = new Deferred(); + $promises = [new Success(1), 2, $deferred->promise(), new Success(4)]; + $result = null; + + $ampAdapter->all($promises)->then(static function ($values) use (&$result) : void { + $result = $values; + }); + + // Resolve the async promise + $deferred->resolve(3); + self::assertSame([1, 2, 3, 4], $result); + } +} diff --git a/tests/Executor/Promise/ReactPromiseAdapterTest.php b/tests/Executor/Promise/ReactPromiseAdapterTest.php index 7e8af70e8..4c3fbb80e 100644 --- a/tests/Executor/Promise/ReactPromiseAdapterTest.php +++ b/tests/Executor/Promise/ReactPromiseAdapterTest.php @@ -20,7 +20,7 @@ */ class ReactPromiseAdapterTest extends TestCase { - public function setUp() + public function setUp() : void { if (class_exists('React\Promise\Promise')) { return; @@ -34,13 +34,13 @@ public function testIsThenableReturnsTrueWhenAReactPromiseIsGiven() : void $reactAdapter = new ReactPromiseAdapter(); self::assertTrue( - $reactAdapter->isThenable(new ReactPromise(static function () { + $reactAdapter->isThenable(new ReactPromise(static function () : void { })) ); self::assertTrue($reactAdapter->isThenable(new FulfilledPromise())); self::assertTrue($reactAdapter->isThenable(new RejectedPromise())); self::assertTrue( - $reactAdapter->isThenable(new LazyPromise(static function () { + $reactAdapter->isThenable(new LazyPromise(static function () : void { })) ); self::assertFalse($reactAdapter->isThenable(false)); @@ -74,7 +74,7 @@ public function testThen() : void $resultPromise = $reactAdapter->then( $promise, - static function ($value) use (&$result) { + static function ($value) use (&$result) : void { $result = $value; } ); @@ -87,7 +87,7 @@ static function ($value) use (&$result) { public function testCreate() : void { $reactAdapter = new ReactPromiseAdapter(); - $resolvedPromise = $reactAdapter->create(static function ($resolve) { + $resolvedPromise = $reactAdapter->create(static function ($resolve) : void { $resolve(1); }); @@ -96,7 +96,7 @@ public function testCreate() : void $result = null; - $resolvedPromise->then(static function ($value) use (&$result) { + $resolvedPromise->then(static function ($value) use (&$result) : void { $result = $value; }); @@ -113,7 +113,7 @@ public function testCreateFulfilled() : void $result = null; - $fulfilledPromise->then(static function ($value) use (&$result) { + $fulfilledPromise->then(static function ($value) use (&$result) : void { $result = $value; }); @@ -132,7 +132,7 @@ public function testCreateRejected() : void $rejectedPromise->then( null, - static function ($error) use (&$exception) { + static function ($error) use (&$exception) : void { $exception = $error; } ); @@ -153,7 +153,7 @@ public function testAll() : void $result = null; - $allPromise->then(static function ($values) use (&$result) { + $allPromise->then(static function ($values) use (&$result) : void { $result = $values; }); @@ -167,7 +167,7 @@ public function testAllShouldPreserveTheOrderOfTheArrayWhenResolvingAsyncPromise $promises = [new FulfilledPromise(1), $deferred->promise(), new FulfilledPromise(3)]; $result = null; - $reactAdapter->all($promises)->then(static function ($values) use (&$result) { + $reactAdapter->all($promises)->then(static function ($values) use (&$result) : void { $result = $values; }); diff --git a/tests/Executor/Promise/SyncPromiseAdapterTest.php b/tests/Executor/Promise/SyncPromiseAdapterTest.php index b9d97b58d..47ec89c68 100644 --- a/tests/Executor/Promise/SyncPromiseAdapterTest.php +++ b/tests/Executor/Promise/SyncPromiseAdapterTest.php @@ -19,7 +19,7 @@ class SyncPromiseAdapterTest extends TestCase /** @var SyncPromiseAdapter */ private $promises; - public function setUp() + public function setUp() : void { $this->promises = new SyncPromiseAdapter(); } @@ -28,7 +28,7 @@ public function testIsThenable() : void { self::assertEquals( true, - $this->promises->isThenable(new Deferred(static function () { + $this->promises->isThenable(new Deferred(static function () : void { })) ); self::assertEquals(false, $this->promises->isThenable(false)); @@ -43,7 +43,7 @@ public function testIsThenable() : void public function testConvert() : void { - $dfd = new Deferred(static function () { + $dfd = new Deferred(static function () : void { }); $result = $this->promises->convertThenable($dfd); @@ -57,7 +57,7 @@ public function testConvert() : void public function testThen() : void { - $dfd = new Deferred(static function () { + $dfd = new Deferred(static function () : void { }); $promise = $this->promises->convertThenable($dfd); @@ -69,13 +69,13 @@ public function testThen() : void public function testCreatePromise() : void { - $promise = $this->promises->create(static function ($resolve, $reject) { + $promise = $this->promises->create(static function ($resolve, $reject) : void { }); self::assertInstanceOf('GraphQL\Executor\Promise\Promise', $promise); self::assertInstanceOf('GraphQL\Executor\Promise\Adapter\SyncPromise', $promise->adoptedPromise); - $promise = $this->promises->create(static function ($resolve, $reject) { + $promise = $this->promises->create(static function ($resolve, $reject) : void { $resolve('A'); }); @@ -93,11 +93,11 @@ private static function assertValidPromise($promise, $expectedNextReason, $expec $onRejectedCalled = false; $promise->then( - static function ($nextValue) use (&$actualNextValue, &$onFulfilledCalled) { + static function ($nextValue) use (&$actualNextValue, &$onFulfilledCalled) : void { $onFulfilledCalled = true; $actualNextValue = $nextValue; }, - static function (Throwable $reason) use (&$actualNextReason, &$onRejectedCalled) { + static function (Throwable $reason) use (&$actualNextReason, &$onRejectedCalled) : void { $onRejectedCalled = true; $actualNextReason = $reason->getMessage(); } @@ -141,7 +141,7 @@ public function testCreatePromiseAll() : void $promise1 = new SyncPromise(); $promise2 = new SyncPromise(); $promise3 = $promise2->then( - static function ($value) { + static function ($value) : string { return $value . '-value3'; } ); @@ -173,12 +173,12 @@ public function testWait() : void { $called = []; - $deferred1 = new Deferred(static function () use (&$called) { + $deferred1 = new Deferred(static function () use (&$called) : int { $called[] = 1; return 1; }); - $deferred2 = new Deferred(static function () use (&$called) { + $deferred2 = new Deferred(static function () use (&$called) : int { $called[] = 2; return 2; @@ -187,8 +187,8 @@ public function testWait() : void $p1 = $this->promises->convertThenable($deferred1); $p2 = $this->promises->convertThenable($deferred2); - $p3 = $p2->then(function () use (&$called) { - $dfd = new Deferred(static function () use (&$called) { + $p3 = $p2->then(function () use (&$called) : Promise { + $dfd = new Deferred(static function () use (&$called) : int { $called[] = 3; return 3; @@ -198,7 +198,7 @@ public function testWait() : void }); $p4 = $p3->then(static function () use (&$called) { - return new Deferred(static function () use (&$called) { + return new Deferred(static function () use (&$called) : int { $called[] = 4; return 4; @@ -208,10 +208,13 @@ public function testWait() : void $all = $this->promises->all([0, $p1, $p2, $p3, $p4]); $result = $this->promises->wait($p2); + + // Having single promise queue means that we won't stop in wait + // until all pending promises are resolved self::assertEquals(2, $result); - self::assertEquals(SyncPromise::PENDING, $p3->adoptedPromise->state); - self::assertEquals(SyncPromise::PENDING, $all->adoptedPromise->state); - self::assertEquals([1, 2], $called); + self::assertEquals(SyncPromise::FULFILLED, $p3->adoptedPromise->state); + self::assertEquals(SyncPromise::FULFILLED, $all->adoptedPromise->state); + self::assertEquals([1, 2, 3, 4], $called); $expectedResult = [0, 1, 2, 3, 4]; $result = $this->promises->wait($all); diff --git a/tests/Executor/Promise/SyncPromiseTest.php b/tests/Executor/Promise/SyncPromiseTest.php index 732f95e8b..b7e117000 100644 --- a/tests/Executor/Promise/SyncPromiseTest.php +++ b/tests/Executor/Promise/SyncPromiseTest.php @@ -23,11 +23,11 @@ public function getFulfilledPromiseResolveData() return $value; }; - $onFulfilledReturnsOtherValue = static function ($value) { + $onFulfilledReturnsOtherValue = static function ($value) : string { return 'other-' . $value; }; - $onFulfilledThrows = static function ($value) { + $onFulfilledThrows = static function ($value) : void { throw new Exception('onFulfilled throws this!'); }; @@ -101,7 +101,7 @@ public function testFulfilledPromise( $nextPromise = $promise->then( null, - static function () { + static function () : void { } ); self::assertSame($promise, $nextPromise); @@ -109,7 +109,7 @@ static function () { $onRejectedCalled = false; $nextPromise = $promise->then( $onFulfilled, - static function () use (&$onRejectedCalled) { + static function () use (&$onRejectedCalled) : void { $onRejectedCalled = true; } ); @@ -149,11 +149,11 @@ private static function assertValidPromise( $onRejectedCalled = false; $promise->then( - static function ($nextValue) use (&$actualNextValue, &$onFulfilledCalled) { + static function ($nextValue) use (&$actualNextValue, &$onFulfilledCalled) : void { $onFulfilledCalled = true; $actualNextValue = $nextValue; }, - static function (Throwable $reason) use (&$actualNextReason, &$onRejectedCalled) { + static function (Throwable $reason) use (&$actualNextReason, &$onRejectedCalled) : void { $onRejectedCalled = true; $actualNextReason = $reason->getMessage(); } @@ -178,15 +178,15 @@ public function getRejectedPromiseData() return null; }; - $onRejectedReturnsSomeValue = static function ($reason) { + $onRejectedReturnsSomeValue = static function ($reason) : string { return 'some-value'; }; - $onRejectedThrowsSameReason = static function ($reason) { + $onRejectedThrowsSameReason = static function ($reason) : void { throw $reason; }; - $onRejectedThrowsOtherReason = static function ($value) { + $onRejectedThrowsOtherReason = static function ($value) : void { throw new Exception('onRejected throws other!'); }; @@ -273,7 +273,7 @@ public function testRejectedPromise( } $nextPromise = $promise->then( - static function () { + static function () : void { }, null ); @@ -281,7 +281,7 @@ static function () { $onFulfilledCalled = false; $nextPromise = $promise->then( - static function () use (&$onFulfilledCalled) { + static function () use (&$onFulfilledCalled) : void { $onFulfilledCalled = true; }, $onRejected @@ -358,7 +358,7 @@ public function testPendingPromise() : void $promise = new SyncPromise(); $promise2 = $promise->then( null, - static function () { + static function () : string { return 'value'; } ); @@ -384,13 +384,13 @@ public function testPendingPromiseThen() : void // Make sure that it queues derivative promises until resolution: $onFulfilledCount = 0; $onRejectedCount = 0; - $onFulfilled = static function ($value) use (&$onFulfilledCount) { + $onFulfilled = static function ($value) use (&$onFulfilledCount) : int { $onFulfilledCount++; return $onFulfilledCount; }; - $onRejected = static function ($reason) use (&$onRejectedCount) { + $onRejected = static function ($reason) use (&$onRejectedCount) : void { $onRejectedCount++; throw $reason; }; diff --git a/tests/Executor/ResolveTest.php b/tests/Executor/ResolveTest.php index 0baddef0f..c4ca4e62d 100644 --- a/tests/Executor/ResolveTest.php +++ b/tests/Executor/ResolveTest.php @@ -16,6 +16,7 @@ class ResolveTest extends TestCase { // Execute: resolve function + /** * @see it('default function accesses properties') */ @@ -50,7 +51,7 @@ public function testDefaultFunctionCallsClosures() : void $_secret = 'secretValue' . uniqid(); $source = [ - 'test' => static function () use ($_secret) { + 'test' => static function () use ($_secret) : string { return $_secret; }, ]; diff --git a/tests/Executor/SyncTest.php b/tests/Executor/SyncTest.php index e2255facd..f05df29e8 100644 --- a/tests/Executor/SyncTest.php +++ b/tests/Executor/SyncTest.php @@ -13,6 +13,7 @@ use GraphQL\Executor\Promise\Promise; use GraphQL\GraphQL; use GraphQL\Language\Parser; +use GraphQL\Tests\PHPUnit\ArraySubsetAsserts; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; @@ -22,13 +23,15 @@ class SyncTest extends TestCase { + use ArraySubsetAsserts; + /** @var Schema */ private $schema; /** @var SyncPromiseAdapter */ private $promiseAdapter; - public function setUp() + public function setUp() : void { $this->schema = new Schema([ 'query' => new ObjectType([ @@ -199,7 +202,7 @@ public function testDoesNotReturnAPromiseForValidationErrors() : void $expected = [ 'errors' => Utils::map( $validationErrors, - static function ($e) { + static function ($e) : array { return FormattedError::createFromException($e); } ), diff --git a/tests/Executor/TestClasses/Adder.php b/tests/Executor/TestClasses/Adder.php index b086b51a1..e28814cda 100644 --- a/tests/Executor/TestClasses/Adder.php +++ b/tests/Executor/TestClasses/Adder.php @@ -16,7 +16,7 @@ public function __construct(float $num) { $this->num = $num; - $this->test = function ($source, $args, $context) { + $this->test = function ($objectValue, $args, $context) : float { return $this->num + $args['addend1'] + $context['addend2']; }; } diff --git a/tests/Executor/TestClasses/Cat.php b/tests/Executor/TestClasses/Cat.php index 500e6a977..cc3caada0 100644 --- a/tests/Executor/TestClasses/Cat.php +++ b/tests/Executor/TestClasses/Cat.php @@ -12,9 +12,21 @@ class Cat /** @var bool */ public $meows; + /** @var Cat|null */ + public $mother; + + /** @var Cat|null */ + public $father; + + /** @var array */ + public $progeny; + public function __construct(string $name, bool $meows) { - $this->name = $name; - $this->meows = $meows; + $this->name = $name; + $this->meows = $meows; + $this->mother = null; + $this->father = null; + $this->progeny = []; } } diff --git a/tests/Executor/TestClasses/ComplexScalar.php b/tests/Executor/TestClasses/ComplexScalar.php index 91197a33e..2ca7a4ee1 100644 --- a/tests/Executor/TestClasses/ComplexScalar.php +++ b/tests/Executor/TestClasses/ComplexScalar.php @@ -5,6 +5,7 @@ namespace GraphQL\Tests\Executor\TestClasses; use GraphQL\Error\Error; +use GraphQL\Language\AST\Node; use GraphQL\Type\Definition\ScalarType; use GraphQL\Utils\Utils; @@ -45,7 +46,7 @@ public function parseValue($value) /** * {@inheritdoc} */ - public function parseLiteral($valueNode, ?array $variables = null) + public function parseLiteral(Node $valueNode, ?array $variables = null) { if ($valueNode->value === 'SerializedValue') { return 'DeserializedValue'; diff --git a/tests/Executor/TestClasses/Dog.php b/tests/Executor/TestClasses/Dog.php index 46861c38b..332d00a3f 100644 --- a/tests/Executor/TestClasses/Dog.php +++ b/tests/Executor/TestClasses/Dog.php @@ -12,9 +12,21 @@ class Dog /** @var bool */ public $woofs; + /** @var Dog|null */ + public $mother; + + /** @var Dog|null */ + public $father; + + /** @var array */ + public $progeny; + public function __construct(string $name, bool $woofs) { - $this->name = $name; - $this->woofs = $woofs; + $this->name = $name; + $this->woofs = $woofs; + $this->mother = null; + $this->father = null; + $this->progeny = []; } } diff --git a/tests/Executor/TestClasses/Root.php b/tests/Executor/TestClasses/Root.php index 6263bd9e6..d77d9459c 100644 --- a/tests/Executor/TestClasses/Root.php +++ b/tests/Executor/TestClasses/Root.php @@ -19,7 +19,7 @@ public function __construct(float $originalNumber) public function promiseToChangeTheNumber($newNumber) : Deferred { - return new Deferred(function () use ($newNumber) { + return new Deferred(function () use ($newNumber) : NumberHolder { return $this->immediatelyChangeTheNumber($newNumber); }); } @@ -38,7 +38,7 @@ public function failToChangeTheNumber() : void public function promiseAndFailToChangeTheNumber() : Deferred { - return new Deferred(function () { + return new Deferred(function () : void { $this->failToChangeTheNumber(); }); } diff --git a/tests/Executor/UnionInterfaceTest.php b/tests/Executor/UnionInterfaceTest.php index f8ce41a2f..cd47d1e6f 100644 --- a/tests/Executor/UnionInterfaceTest.php +++ b/tests/Executor/UnionInterfaceTest.php @@ -4,13 +4,19 @@ namespace GraphQL\Tests\Executor; +use GraphQL\Error\DebugFlag; +use GraphQL\Error\InvariantViolation; use GraphQL\Executor\Executor; use GraphQL\GraphQL; +use GraphQL\Language\AST\FieldDefinitionNode; +use GraphQL\Language\AST\FieldNode; +use GraphQL\Language\AST\NodeList; use GraphQL\Language\Parser; use GraphQL\Tests\Executor\TestClasses\Cat; use GraphQL\Tests\Executor\TestClasses\Dog; use GraphQL\Tests\Executor\TestClasses\Person; use GraphQL\Type\Definition\InterfaceType; +use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; @@ -20,7 +26,7 @@ class UnionInterfaceTest extends TestCase { - /** @var */ + /** @var Schema */ public $schema; /** @var Cat */ @@ -35,7 +41,7 @@ class UnionInterfaceTest extends TestCase /** @var Person */ public $john; - public function setUp() + public function setUp() : void { $NamedType = new InterfaceType([ 'name' => 'Named', @@ -44,26 +50,57 @@ public function setUp() ], ]); + $LifeType = new InterfaceType([ + 'name' => 'Life', + 'fields' => static function () use (&$LifeType) : array { + return [ + 'progeny' => ['type' => Type::listOf($LifeType)], + ]; + }, + ]); + + $MammalType = new InterfaceType([ + 'name' => 'Mammal', + 'interfaces' => [$LifeType], + 'fields' => static function () use (&$MammalType) : array { + return [ + 'progeny' => ['type' => Type::listOf($MammalType)], + 'mother' => ['type' => &$MammalType], + 'father' => ['type' => &$MammalType], + ]; + }, + ]); + $DogType = new ObjectType([ 'name' => 'Dog', - 'interfaces' => [$NamedType], - 'fields' => [ - 'name' => ['type' => Type::string()], - 'woofs' => ['type' => Type::boolean()], - ], - 'isTypeOf' => static function ($value) { + 'interfaces' => [$MammalType, $LifeType, $NamedType], + 'fields' => static function () use (&$DogType) : array { + return [ + 'name' => ['type' => Type::string()], + 'woofs' => ['type' => Type::boolean()], + 'progeny' => ['type' => Type::listOf($DogType)], + 'mother' => ['type' => &$DogType], + 'father' => ['type' => &$DogType], + ]; + }, + 'isTypeOf' => static function ($value) : bool { return $value instanceof Dog; }, ]); $CatType = new ObjectType([ 'name' => 'Cat', - 'interfaces' => [$NamedType], - 'fields' => [ - 'name' => ['type' => Type::string()], - 'meows' => ['type' => Type::boolean()], - ], - 'isTypeOf' => static function ($value) { + 'interfaces' => [$MammalType, $LifeType, $NamedType], + 'fields' => static function () use (&$CatType) : array { + return [ + 'name' => ['type' => Type::string()], + 'meows' => ['type' => Type::boolean()], + 'progeny' => ['type' => Type::listOf($CatType)], + 'mother' => ['type' => &$CatType], + 'father' => ['type' => &$CatType], + ]; + }, + 'isTypeOf' => static function ($value) : bool { return $value instanceof Cat; }, ]); @@ -71,25 +108,32 @@ public function setUp() $PetType = new UnionType([ 'name' => 'Pet', 'types' => [$DogType, $CatType], - 'resolveType' => static function ($value) use ($DogType, $CatType) { + 'resolveType' => static function ($value) use ($DogType, $CatType) : ObjectType { if ($value instanceof Dog) { return $DogType; } if ($value instanceof Cat) { return $CatType; } + + throw new InvariantViolation('Unknown type'); }, ]); $PersonType = new ObjectType([ 'name' => 'Person', - 'interfaces' => [$NamedType], - 'fields' => [ - 'name' => ['type' => Type::string()], - 'pets' => ['type' => Type::listOf($PetType)], - 'friends' => ['type' => Type::listOf($NamedType)], - ], - 'isTypeOf' => static function ($value) { + 'interfaces' => [$NamedType, $MammalType, $LifeType], + 'fields' => static function () use (&$PetType, &$NamedType, &$PersonType) : array { + return [ + 'name' => ['type' => Type::string()], + 'pets' => ['type' => Type::listOf($PetType)], + 'friends' => ['type' => Type::listOf($NamedType)], + 'progeny' => ['type' => Type::listOf($PersonType)], + 'mother' => ['type' => $PersonType], + 'father' => ['type' => $PersonType], + ]; + }, + 'isTypeOf' => static function ($value) : bool { return $value instanceof Person; }, ]); @@ -99,10 +143,16 @@ public function setUp() 'types' => [$PetType], ]); - $this->garfield = new Cat('Garfield', false); - $this->odie = new Dog('Odie', true); - $this->liz = new Person('Liz'); - $this->john = new Person('John', [$this->garfield, $this->odie], [$this->liz, $this->odie]); + $this->garfield = new Cat('Garfield', false); + $this->garfield->mother = new Cat("Garfield's Mom", false); + $this->garfield->mother->progeny = [$this->garfield]; + + $this->odie = new Dog('Odie', true); + $this->odie->mother = new Dog("Odie's Mom", true); + $this->odie->mother->progeny = [$this->odie]; + + $this->liz = new Person('Liz'); + $this->john = new Person('John', [$this->garfield, $this->odie], [$this->liz, $this->odie]); } // Execute: Union and intersection types @@ -123,6 +173,15 @@ interfaces { name } enumValues { name } inputFields { name } } + Mammal: __type(name: "Mammal") { + kind + name + fields { name } + interfaces { name } + possibleTypes { name } + enumValues { name } + inputFields { name } + } Pet: __type(name: "Pet") { kind name @@ -143,7 +202,26 @@ enumValues { name } 'fields' => [ ['name' => 'name'], ], - 'interfaces' => null, + 'interfaces' => [], + 'possibleTypes' => [ + ['name' => 'Person'], + ['name' => 'Dog'], + ['name' => 'Cat'], + ], + 'enumValues' => null, + 'inputFields' => null, + ], + 'Mammal' => [ + 'kind' => 'INTERFACE', + 'name' => 'Mammal', + 'fields' => [ + ['name' => 'progeny'], + ['name' => 'mother'], + ['name' => 'father'], + ], + 'interfaces' => [ + ['name' => 'Life'], + ], 'possibleTypes' => [ ['name' => 'Person'], ['name' => 'Dog'], @@ -192,8 +270,16 @@ public function testExecutesUsingUnionTypes() : void '__typename' => 'Person', 'name' => 'John', 'pets' => [ - ['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => false], - ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true], + [ + '__typename' => 'Cat', + 'name' => 'Garfield', + 'meows' => false, + ], + [ + '__typename' => 'Dog', + 'name' => 'Odie', + 'woofs' => true, + ], ], ], ]; @@ -229,8 +315,16 @@ public function testExecutesUnionTypesWithInlineFragments() : void '__typename' => 'Person', 'name' => 'John', 'pets' => [ - ['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => false], - ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true], + [ + '__typename' => 'Cat', + 'name' => 'Garfield', + 'meows' => false, + ], + [ + '__typename' => 'Dog', + 'name' => 'Odie', + 'woofs' => true, + ], ], ], @@ -289,6 +383,20 @@ public function testExecutesInterfaceTypesWithInlineFragments() : void ... on Cat { meows } + + ... on Mammal { + mother { + __typename + ... on Dog { + name + woofs + } + ... on Cat { + name + meows + } + } + } } } '); @@ -297,13 +405,26 @@ public function testExecutesInterfaceTypesWithInlineFragments() : void '__typename' => 'Person', 'name' => 'John', 'friends' => [ - ['__typename' => 'Person', 'name' => 'Liz'], - ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true], + [ + '__typename' => 'Person', + 'name' => 'Liz', + 'mother' => null, + ], + [ + '__typename' => 'Dog', + 'name' => 'Odie', + 'woofs' => true, + 'mother' => [ + '__typename' => 'Dog', + 'name' => "Odie's Mom", + 'woofs' => true, + ], + ], ], ], ]; - self::assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray(true)); + self::assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)); } /** @@ -315,7 +436,14 @@ public function testAllowsFragmentConditionsToBeAbstractTypes() : void { __typename name - pets { ...PetFields } + pets { + ...PetFields, + ...on Mammal { + mother { + ...ProgenyFields + } + } + } friends { ...FriendFields } } @@ -341,6 +469,12 @@ public function testAllowsFragmentConditionsToBeAbstractTypes() : void meows } } + + fragment ProgenyFields on Life { + progeny { + __typename + } + } '); $expected = [ @@ -348,12 +482,37 @@ public function testAllowsFragmentConditionsToBeAbstractTypes() : void '__typename' => 'Person', 'name' => 'John', 'pets' => [ - ['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => false], - ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true], + [ + '__typename' => 'Cat', + 'name' => 'Garfield', + 'meows' => false, + 'mother' => [ + 'progeny' => [ + ['__typename' => 'Cat'], + ], + ], + ], + [ + '__typename' => 'Dog', + 'name' => 'Odie', + 'woofs' => true, + 'mother' => [ + 'progeny' => [ + ['__typename' => 'Dog'], + ], + ], + ], ], 'friends' => [ - ['__typename' => 'Person', 'name' => 'Liz'], - ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true], + [ + '__typename' => 'Person', + 'name' => 'Liz', + ], + [ + '__typename' => 'Dog', + 'name' => 'Odie', + 'woofs' => true, + ], ], ], ]; diff --git a/tests/Executor/VariablesTest.php b/tests/Executor/VariablesTest.php index 533beb5ea..e7a97de70 100644 --- a/tests/Executor/VariablesTest.php +++ b/tests/Executor/VariablesTest.php @@ -4,16 +4,19 @@ namespace GraphQL\Tests\Executor; -use GraphQL\Error\Error; use GraphQL\Executor\Executor; use GraphQL\Language\Parser; use GraphQL\Tests\Executor\TestClasses\ComplexScalar; +use GraphQL\Tests\PHPUnit\ArraySubsetAsserts; +use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; +use GraphQL\Utils\Utils; use PHPUnit\Framework\TestCase; -use function json_encode; +use function acos; +use function array_key_exists; /** * Execute: Handles inputs @@ -21,6 +24,8 @@ */ class VariablesTest extends TestCase { + use ArraySubsetAsserts; + public function testUsingInlineStructs() : void { // executes with complex input: @@ -45,6 +50,18 @@ public function testUsingInlineStructs() : void self::assertEquals($expected, $result->toArray()); + $result = $this->executeQuery( + ' + query ($input: TestInputObject) { + fieldWithObjectInput(input: $input) + } + ', + ['input' => ['a' => 'foo', 'b' => 'bar', 'c' => 'baz']] + ); + $expected = ['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']]; + + self::assertEquals($expected, $result->toArray()); + // properly parses null value to null $result = $this->executeQuery(' { @@ -102,9 +119,6 @@ private function executeQuery($query, $variableValues = null) return Executor::execute($this->schema(), $document, null, null, $variableValues); } - /** - * Describe: Handles nullable scalars - */ public function schema() : Schema { $ComplexScalarType = ComplexScalar::create(); @@ -127,9 +141,22 @@ public function schema() : Schema ], ]); + $TestEnum = new EnumType([ + 'name' => 'TestEnum', + 'values' => [ + 'NULL' => [ 'value' => null ], + 'NAN' => [ 'value' => acos(8) ], + 'FALSE' => [ 'value' => false ], + 'CUSTOM' => [ 'value' => 'custom value' ], + 'DEFAULT_VALUE' => [], + ], + ]); + $TestType = new ObjectType([ 'name' => 'TestType', 'fields' => [ + 'fieldWithEnumInput' => $this->fieldWithInputArg(['type' => $TestEnum]), + 'fieldWithNonNullableEnumInput' => $this->fieldWithInputArg(['type' => Type::nonNull($TestEnum)]), 'fieldWithObjectInput' => $this->fieldWithInputArg(['type' => $TestInputObject]), 'fieldWithNullableStringInput' => $this->fieldWithInputArg(['type' => Type::string()]), 'fieldWithNonNullableStringInput' => $this->fieldWithInputArg(['type' => Type::nonNull(Type::string())]), @@ -137,6 +164,10 @@ public function schema() : Schema 'type' => Type::string(), 'defaultValue' => 'Hello World', ]), + 'fieldWithNonNullableStringInputAndDefaultArgumentValue' => $this->fieldWithInputArg([ + 'type' => Type::nonNull(Type::string()), + 'defaultValue' => 'Hello World', + ]), 'fieldWithNestedInputObject' => $this->fieldWithInputArg([ 'type' => $TestNestedInputObject, 'defaultValue' => 'Hello World', @@ -156,9 +187,12 @@ private function fieldWithInputArg($inputArg) return [ 'type' => Type::string(), 'args' => ['input' => $inputArg], - 'resolve' => static function ($_, $args) { + 'resolve' => static function ($_, $args) : ?string { if (isset($args['input'])) { - return json_encode($args['input']); + return Utils::printSafeJson($args['input']); + } + if (array_key_exists('input', $args) && $args['input'] === null) { + return 'null'; } return null; @@ -183,6 +217,33 @@ public function testUsingVariables() : void $result->toArray() ); + // uses undefined when variable not provided + $result = $this->executeQuery( + ' + query q($input: String) { + fieldWithNullableStringInput(input: $input) + }', + [] // Intentionally missing variable values. + ); + $expected = [ + 'data' => ['fieldWithNullableStringInput' => null], + ]; + self::assertEquals($expected, $result->toArray()); + + // uses null when variable provided explicit null value + $result = $this->executeQuery( + ' + query q($input: String) { + fieldWithNullableStringInput(input: $input) + }', + [ 'input' => null ] + ); + + $expected = [ + 'data' => ['fieldWithNullableStringInput' => 'null'], + ]; + self::assertEquals($expected, $result->toArray()); + // uses default value when not provided: $result = $this->executeQuery(' query ($input: TestInputObject = {a: "foo", b: ["bar"], c: "baz"}) { @@ -195,6 +256,47 @@ public function testUsingVariables() : void ]; self::assertEquals($expected, $result->toArray()); + // does not use default value when provided + $result = $this->executeQuery( + 'query q($input: String = "Default value") { + fieldWithNullableStringInput(input: $input) + }', + [ 'input' => 'Variable value' ] + ); + + $expected = [ + 'data' => ['fieldWithNullableStringInput' => '"Variable value"'], + ]; + self::assertEquals($expected, $result->toArray()); + + // uses explicit null value instead of default value + $result = $this->executeQuery( + ' + query q($input: String = "Default value") { + fieldWithNullableStringInput(input: $input) + }', + [ 'input' => null ] + ); + + $expected = [ + 'data' => ['fieldWithNullableStringInput' => 'null'], + ]; + self::assertEquals($expected, $result->toArray()); + + // uses null default value when not provided + $result = $this->executeQuery( + ' + query q($input: String = null) { + fieldWithNullableStringInput(input: $input) + }', + [] // Intentionally missing variable values. + ); + + $expected = [ + 'data' => ['fieldWithNullableStringInput' => 'null'], + ]; + self::assertEquals($expected, $result->toArray()); + // properly parses single value to list: $params = ['input' => ['a' => 'foo', 'b' => 'bar', 'c' => 'baz']]; $result = $this->executeQuery($doc, $params); @@ -309,6 +411,76 @@ public function testUsingVariables() : void self::assertEquals($expected, $result->toArray()); } + public function testUsingStdClassVariables() : void + { + $doc = ' + query q($input:TestNestedInputObject) { + fieldWithNestedInputObject(input: $input) + } + '; + + // executes with complex input: + $params = ['input' => (object) ['na' => (object) ['a' => 'foo', 'b' => ['bar'], 'c' => 'baz'], 'nb' => 'test']]; + $result = $this->executeQuery($doc, $params); + + self::assertEquals( + ['data' => ['fieldWithNestedInputObject' => '{"na":{"a":"foo","b":["bar"],"c":"baz"},"nb":"test"}']], + $result->toArray() + ); + } + + /** + * Describe: Handles custom enum values + */ + + /** + * @see it('allows custom enum values as inputs') + */ + public function testAllowsCustomEnumValuesAsInputs() + { + $result = $this->executeQuery(' + { + null: fieldWithEnumInput(input: NULL) + NaN: fieldWithEnumInput(input: NAN) + false: fieldWithEnumInput(input: FALSE) + customValue: fieldWithEnumInput(input: CUSTOM) + defaultValue: fieldWithEnumInput(input: DEFAULT_VALUE) + } + '); + + $expected = [ + 'data' => [ + 'null' => 'null', + 'NaN' => 'NAN', + 'false' => 'false', + 'customValue' => '"custom value"', + 'defaultValue' => '"DEFAULT_VALUE"', + ], + ]; + self::assertEquals($expected, $result->toArray()); + } + + /** + * @see it('allows non-nullable inputs to have null as enum custom value') + */ + public function testAllowsNonNullableInputsToHaveNullAsEnumCustomValue() + { + $result = $this->executeQuery(' + { + fieldWithNonNullableEnumInput(input: NULL) + } + '); + + self::assertEquals( + ['data' => ['fieldWithNonNullableEnumInput' => 'null']], + $result->toArray() + ); + } + + /** + * Describe: Handles nullable scalars + */ + /** * @see it('allows nullable inputs to be omitted') */ @@ -454,10 +626,8 @@ public function testDoesNotAllowNonNullableInputsToBeSetToNullInAVariable() : vo $expected = [ 'errors' => [ [ - 'message' => - 'Variable "$value" got invalid value null; ' . - 'Expected non-nullable type String! not to be null.', - 'locations' => [['line' => 2, 'column' => 31]], + 'message' => 'Variable "$value" of non-null type "String!" must not be null.', + 'locations' => [['line' => 2, 'column' => 31]], 'extensions' => ['category' => 'graphql'], ], ], @@ -536,7 +706,7 @@ public function testReportsErrorForArrayPassedIntoStringInput() : void 'errors' => [[ 'message' => 'Variable "$value" got invalid value [1,2,3]; Expected type ' . - 'String; String cannot represent an array value: [1,2,3]', + 'String; String cannot represent a non string value: [1,2,3]', 'locations' => [ ['line' => 2, 'column' => 31], ], @@ -547,16 +717,6 @@ public function testReportsErrorForArrayPassedIntoStringInput() : void self::assertEquals($expected, $result->toArray()); } - /** - * @see it('serializing an array via GraphQLString throws TypeError') - */ - public function testSerializingAnArrayViaGraphQLStringThrowsTypeError() : void - { - $this->expectException(Error::class); - $this->expectExceptionMessage('String cannot represent non scalar value: [1,2,3]'); - Type::string()->serialize([1, 2, 3]); - } - /** * @see it('reports error for non-provided variables for non-nullable inputs') */ @@ -598,7 +758,7 @@ public function testAllowsListsToBeNull() : void } '; $result = $this->executeQuery($doc, ['input' => null]); - $expected = ['data' => ['list' => null]]; + $expected = ['data' => ['list' => 'null']]; self::assertEquals($expected, $result->toArray()); } @@ -647,10 +807,8 @@ public function testDoesNotAllowNonNullListsToBeNull() : void $expected = [ 'errors' => [ [ - 'message' => - 'Variable "$input" got invalid value null; ' . - 'Expected non-nullable type [String]! not to be null.', - 'locations' => [['line' => 2, 'column' => 17]], + 'message' => 'Variable "$input" of non-null type "[String]!" must not be null.', + 'locations' => [['line' => 2, 'column' => 17]], 'extensions' => ['category' => 'graphql'], ], ], @@ -699,7 +857,7 @@ public function testAllowsListsOfNonNullsToBeNull() : void } '; $result = $this->executeQuery($doc, ['input' => null]); - $expected = ['data' => ['listNN' => null]]; + $expected = ['data' => ['listNN' => 'null']]; self::assertEquals($expected, $result->toArray()); } @@ -757,10 +915,8 @@ public function testDoesNotAllowNonNullListsOfNonNullsToBeNull() : void $expected = [ 'errors' => [ [ - 'message' => - 'Variable "$input" got invalid value null; ' . - 'Expected non-nullable type [String!]! not to be null.', - 'locations' => [['line' => 2, 'column' => 17]], + 'message' => 'Variable "$input" of non-null type "[String!]!" must not be null.', + 'locations' => [['line' => 2, 'column' => 17]], 'extensions' => ['category' => 'graphql'], ], ], @@ -916,4 +1072,21 @@ public function testNotWhenArgumentCannotBeCoerced() : void self::assertEquals($expected, $result->toArray()); } + + /** + * @see it('when no runtime value is provided to a non-null argument') + */ + public function testWhenNoRuntimeValueIsProvidedToANonNullArgument() + { + $result = $this->executeQuery(' + query optionalVariable($optional: String) { + fieldWithNonNullableStringInputAndDefaultArgumentValue(input: $optional) + } + '); + + $expected = [ + 'data' => ['fieldWithNonNullableStringInputAndDefaultArgumentValue' => '"Hello World"'], + ]; + self::assertEquals($expected, $result->toArray()); + } } diff --git a/tests/Experimental/Executor/CollectorTest.php b/tests/Experimental/Executor/CollectorTest.php index c078cabdb..24b14f70b 100644 --- a/tests/Experimental/Executor/CollectorTest.php +++ b/tests/Experimental/Executor/CollectorTest.php @@ -4,12 +4,12 @@ namespace GraphQL\Tests\Experimental\Executor; +use GraphQL\Error\DebugFlag; use GraphQL\Error\FormattedError; use GraphQL\Experimental\Executor\Collector; use GraphQL\Experimental\Executor\Runtime; use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\Node; -use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\ValueNode; use GraphQL\Language\Parser; @@ -22,6 +22,7 @@ use Throwable; use function array_map; use function basename; +use function count; use function file_exists; use function file_put_contents; use function json_encode; @@ -71,21 +72,21 @@ public function addError($error) $pipeline = []; foreach ($collector->collectFields($collector->rootType, $collector->operation->selectionSet) as $shared) { $execution = new stdClass(); - if (! empty($shared->fieldNodes)) { - $execution->fieldNodes = array_map(static function (Node $node) { + if (count($shared->fieldNodes ?? []) > 0) { + $execution->fieldNodes = array_map(static function (Node $node) : array { return $node->toArray(true); }, $shared->fieldNodes); } - if (! empty($shared->fieldName)) { + if (strlen($shared->fieldName ?? '') > 0) { $execution->fieldName = $shared->fieldName; } - if (! empty($shared->resultName)) { + if (strlen($shared->resultName ?? '') > 0) { $execution->resultName = $shared->resultName; } - if (! empty($shared->argumentValueMap)) { + if (isset($shared->argumentValueMap)) { $execution->argumentValueMap = []; + /** @var Node $valueNode */ foreach ($shared->argumentValueMap as $argumentName => $valueNode) { - /** @var Node $valueNode */ $execution->argumentValueMap[$argumentName] = $valueNode->toArray(true); } } @@ -105,13 +106,13 @@ public function addError($error) } $result = []; - if (! empty($runtime->errors)) { + if (count($runtime->errors) > 0) { $result['errors'] = array_map( - FormattedError::prepareFormatter(null, false), + FormattedError::prepareFormatter(null, DebugFlag::NONE), $runtime->errors ); } - if (! empty($pipeline)) { + if (count($pipeline) > 0) { $result['pipeline'] = $pipeline; } @@ -360,10 +361,9 @@ public function provideForTestCollectFields() foreach ($testCases as [$schema, $query, $variableValues]) { $documentNode = Parser::parse($query, ['noLocation' => true]); $operationName = null; + /** @var Node $definitionNode */ foreach ($documentNode->definitions as $definitionNode) { - /** @var Node $definitionNode */ - if ($definitionNode->kind === NodeKind::OPERATION_DEFINITION) { - /** @var OperationDefinitionNode $definitionNode */ + if ($definitionNode instanceof OperationDefinitionNode) { self::assertNotNull($definitionNode->name); $operationName = $definitionNode->name->value; break; diff --git a/tests/GraphQLTest.php b/tests/GraphQLTest.php index 13193b5a5..32396856c 100644 --- a/tests/GraphQLTest.php +++ b/tests/GraphQLTest.php @@ -5,6 +5,7 @@ namespace GraphQL\Tests; use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter; +use GraphQL\Executor\Promise\Promise; use GraphQL\GraphQL; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; @@ -30,7 +31,7 @@ public function testPromiseToExecute() : void 'type' => Type::nonNull(Type::string()), ], ], - 'resolve' => static function ($value, $args) use ($promiseAdapter) { + 'resolve' => static function ($rootValue, $args) use ($promiseAdapter) : Promise { return $promiseAdapter->createFulfilled(sprintf('Hi %s!', $args['name'])); }, ], diff --git a/tests/IntegerFloatPrimitiveIntrospectionTest.php b/tests/IntegerFloatPrimitiveIntrospectionTest.php new file mode 100644 index 000000000..cf97dee24 --- /dev/null +++ b/tests/IntegerFloatPrimitiveIntrospectionTest.php @@ -0,0 +1,81 @@ + 'Query', + 'fields' => [ + 'test' => [ + 'type' => Type::string(), + 'args' => [ + 'index' => [ + 'description' => 'This fails', + 'type' => Type::int(), + 'defaultValue' => 3, + ], + 'another' => [ + 'description' => 'This fails', + 'type' => Type::float(), + 'defaultValue' => 3.14, + ], + ], + ], + ], + ]); + + return new Schema(['query' => $queryType]); + } + + public function testDefaultValues() : void + { + $query = '{ + __schema { + queryType { + fields { + name + args { + name + defaultValue + } + } + } + } + }'; + + $expected = [ + '__schema' => [ + 'queryType' => [ + 'fields' => [ + [ + 'name' => 'test', + 'args' => [ + [ + 'name' => 'index', + 'defaultValue' => '3', + ], + [ + 'name' => 'another', + 'defaultValue' => '3.14', + ], + ], + ], + ], + ], + ], + ]; + + self::assertSame(['data' => $expected], GraphQL::executeQuery(self::build(), $query)->toArray()); + } +} diff --git a/tests/Language/LexerTest.php b/tests/Language/LexerTest.php index 5e7ed1e71..9e67cd875 100644 --- a/tests/Language/LexerTest.php +++ b/tests/Language/LexerTest.php @@ -9,6 +9,7 @@ use GraphQL\Language\Source; use GraphQL\Language\SourceLocation; use GraphQL\Language\Token; +use GraphQL\Tests\PHPUnit\ArraySubsetAsserts; use GraphQL\Utils\Utils; use PHPUnit\Framework\TestCase; use function count; @@ -16,6 +17,8 @@ class LexerTest extends TestCase { + use ArraySubsetAsserts; + /** * @see it('disallows uncommon control characters') */ @@ -295,6 +298,16 @@ public function testLexesStrings() : void ], (array) $this->lexOne('"\u1234\u5678\u90AB\uCDEF"') ); + + self::assertArraySubset( + [ + 'kind' => Token::STRING, + 'start' => 0, + 'end' => 41, + 'value' => '𝕌𝕋𝔽-16', + ], + (array) $this->lexOne('"\ud835\udd4C\ud835\udd4B\ud835\udd3d-16"') + ); } /** @@ -430,6 +443,14 @@ public function reportsUsefulStringErrors() ['"bad \\uXXXX esc"', "Invalid character escape sequence: \\uXXXX", $this->loc(1, 7)], ['"bad \\uFXXX esc"', "Invalid character escape sequence: \\uFXXX", $this->loc(1, 7)], ['"bad \\uXXXF esc"', "Invalid character escape sequence: \\uXXXF", $this->loc(1, 7)], + ['"bad \\uD835"', 'Invalid UTF-16 trailing surrogate: ', $this->loc(1, 13)], + ['"bad \\uD835\\u1"', "Invalid UTF-16 trailing surrogate: \\u1", $this->loc(1, 13)], + ['"bad \\uD835\\u1 esc"', "Invalid UTF-16 trailing surrogate: \\u1 es", $this->loc(1, 13)], + ['"bad \\uD835uuFFFF esc"', 'Invalid UTF-16 trailing surrogate: uuFFFF', $this->loc(1, 13)], + ['"bad \\uD835\\u0XX1 esc"', "Invalid UTF-16 trailing surrogate: \\u0XX1", $this->loc(1, 13)], + ['"bad \\uD835\\uXXXX esc"', "Invalid UTF-16 trailing surrogate: \\uXXXX", $this->loc(1, 13)], + ['"bad \\uD835\\uFXXX esc"', "Invalid UTF-16 trailing surrogate: \\uFXXX", $this->loc(1, 13)], + ['"bad \\uD835\\uXXXF esc"', "Invalid UTF-16 trailing surrogate: \\uXXXF", $this->loc(1, 13)], ]; } @@ -701,7 +722,7 @@ public function testDoubleLinkedList() : void $tokens = []; for ($tok = $startToken; $tok; $tok = $tok->next) { - if (! empty($tokens)) { + if (count($tokens) > 0) { // Tokens are double-linked, prev should point to last seen token. self::assertSame($tokens[count($tokens) - 1], $tok->prev); } diff --git a/tests/Language/NodeListTest.php b/tests/Language/NodeListTest.php new file mode 100644 index 000000000..f5308472f --- /dev/null +++ b/tests/Language/NodeListTest.php @@ -0,0 +1,44 @@ + 'foo']); + $nodeList['foo'] = $nameNode->toArray(); + + self::assertInstanceOf(get_class($nameNode), $nodeList['foo']); + } + + public function testThrowsOnInvalidArrays() : void + { + $nodeList = new NodeList([]); + + self::expectException(InvariantViolation::class); + $nodeList[] = ['not a valid array representation of an AST node']; + } + + public function testPushNodes() : void + { + $nodeList = new NodeList([]); + self::assertCount(0, $nodeList); + + $nodeList[] = new NameNode(['value' => 'foo']); + self::assertCount(1, $nodeList); + + $nodeList[] = new NameNode(['value' => 'bar']); + self::assertCount(2, $nodeList); + } +} diff --git a/tests/Language/ParserTest.php b/tests/Language/ParserTest.php index d0769e910..0b3c4162b 100644 --- a/tests/Language/ParserTest.php +++ b/tests/Language/ParserTest.php @@ -12,8 +12,11 @@ use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeList; +use GraphQL\Language\AST\ObjectTypeDefinitionNode; +use GraphQL\Language\AST\OperationDefinitionNode; use GraphQL\Language\AST\SelectionSetNode; use GraphQL\Language\AST\StringValueNode; +use GraphQL\Language\AST\VariableNode; use GraphQL\Language\Parser; use GraphQL\Language\Source; use GraphQL\Language\SourceLocation; @@ -139,6 +142,15 @@ public function testParsesConstantDefaultValues() : void ); } + /** + * @see it('parses variable definition directives') + */ + public function testParsesVariableDefinitionDirectives() + { + $this->expectNotToPerformAssertions(); + Parser::parse('query Foo($x: Boolean = false @bar) { field }'); + } + private function expectSyntaxError($text, $message, $location) { $this->expectException(SyntaxError::class); @@ -185,7 +197,7 @@ public function testDoesNotAcceptFragmentSpreadOfOn() : void */ public function testParsesMultiByteCharacters() : void { - // Note: \u0A0A could be naively interpretted as two line-feed chars. + // Note: \u0A0A could be naively interpreted as two line-feed chars. $char = Utils::chr(0x0A0A); $query = <<definitions[0]->selectionSet); + /** @var OperationDefinitionNode $operationDefinition */ + $operationDefinition = $result->definitions[0]; + self::assertEquals($expected, $operationDefinition->selectionSet); } /** @@ -333,7 +347,7 @@ public function testParseCreatesAst() : void '); $result = Parser::parse($source); - $loc = static function ($start, $end) { + $loc = static function (int $start, int $end) : array { return [ 'start' => $start, 'end' => $end, @@ -377,7 +391,7 @@ public function testParseCreatesAst() : void 'loc' => $loc(13, 14), 'value' => '4', ], - 'loc' => $loc(9, 14, $source), + 'loc' => $loc(9, 14), ], ], 'directives' => [], @@ -444,7 +458,7 @@ public function testParseCreatesAstFromNamelessQueryWithoutVariables() : void '); $result = Parser::parse($source); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return [ 'start' => $start, 'end' => $end, @@ -716,4 +730,45 @@ public function testParsesNestedTypes() : void self::nodeToArray(Parser::parseType('[MyType!]')) ); } + + public function testPartiallyParsesSource() : void + { + self::assertInstanceOf( + NameNode::class, + Parser::name('Foo') + ); + + self::assertInstanceOf( + ObjectTypeDefinitionNode::class, + Parser::objectTypeDefinition('type Foo { name: String }') + ); + + self::assertInstanceOf( + VariableNode::class, + Parser::valueLiteral('$foo') + ); + + self::assertInstanceOf( + NodeList::class, + Parser::argumentsDefinition('(foo: Int!)') + ); + + self::assertInstanceOf( + NodeList::class, + Parser::directiveLocations('| INPUT_OBJECT | OBJECT') + ); + + self::assertInstanceOf( + NodeList::class, + Parser::implementsInterfaces('implements Foo & Bar') + ); + + self::assertInstanceOf( + NodeList::class, + Parser::unionMemberTypes('= | Foo | Bar') + ); + + $this->expectException(SyntaxError::class); + Parser::constValueLiteral('$foo'); + } } diff --git a/tests/Language/PrinterTest.php b/tests/Language/PrinterTest.php index 91e851423..c7e774453 100644 --- a/tests/Language/PrinterTest.php +++ b/tests/Language/PrinterTest.php @@ -9,6 +9,8 @@ use GraphQL\Language\AST\NameNode; use GraphQL\Language\Parser; use GraphQL\Language\Printer; +use GraphQL\Type\Definition\Type; +use GraphQL\Utils\AST; use PHPUnit\Framework\TestCase; use Throwable; use function file_get_contents; @@ -93,6 +95,37 @@ public function testCorrectlyPrintsOpsWithoutName() : void self::assertEquals($expected, Printer::doPrint($mutationAstWithArtifacts)); } + /** + * @see it('prints query with variable directives') + */ + public function testPrintsQueryWithVariableDirectives() + { + $queryAstWithVariableDirective = Parser::parse( + 'query ($foo: TestType = {a: 123} @testDirective(if: true) @test) { id }' + ); + $expected = 'query ($foo: TestType = {a: 123} @testDirective(if: true) @test) { + id +} +'; + self::assertEquals($expected, Printer::doPrint($queryAstWithVariableDirective)); + } + + /** + * @see it('prints fragment with variable directives') + */ + public function testPrintsFragmentWithVariableDirectives() + { + $queryAstWithVariableDirective = Parser::parse( + 'fragment Foo($foo: TestType @test) on TestType @testDirective { id }', + ['experimentalFragmentVariables' => true] + ); + $expected = 'fragment Foo($foo: TestType @test) on TestType @testDirective { + id +} +'; + self::assertEquals($expected, Printer::doPrint($queryAstWithVariableDirective)); + } + /** * @see it('correctly prints single-line with leading space') */ @@ -263,4 +296,10 @@ public function testPrintsKitchenSink() : void EOT; self::assertEquals($expected, $printed); } + + public function testPrintPrimitives() : void + { + self::assertSame('3', Printer::doPrint(AST::astFromValue(3, Type::int()))); + self::assertSame('3.14', Printer::doPrint(AST::astFromValue(3.14, Type::float()))); + } } diff --git a/tests/Language/SchemaParserTest.php b/tests/Language/SchemaParserTest.php index d3ce8731a..d03e3937a 100644 --- a/tests/Language/SchemaParserTest.php +++ b/tests/Language/SchemaParserTest.php @@ -4,15 +4,21 @@ namespace GraphQL\Tests\Language; +use GraphQL\Error\DebugFlag; use GraphQL\Error\SyntaxError; use GraphQL\Language\AST\NodeKind; +use GraphQL\Language\DirectiveLocation; use GraphQL\Language\Parser; use GraphQL\Language\SourceLocation; +use GraphQL\Tests\PHPUnit\ArraySubsetAsserts; use PHPUnit\Framework\TestCase; class SchemaParserTest extends TestCase { + use ArraySubsetAsserts; + // Describe: Schema Parser + /** * @see it('Simple type') */ @@ -23,7 +29,7 @@ public function testSimpleType() : void world: String }'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -98,7 +104,7 @@ public function testParsesTypeWithDescriptionString() : void world: String }'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -145,7 +151,7 @@ public function testParsesTypeWithDescriptionMultiLineString() : void world: String }'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -189,7 +195,7 @@ public function testSimpleExtension() : void } '; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -217,13 +223,13 @@ public function testSimpleExtension() : void } /** - * @see it('Extension without fields') + * @see it('Object extension without fields') */ - public function testExtensionWithoutFields() : void + public function testObjectExtensionWithoutFields() : void { $body = 'extend type Hello implements Greeting'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -247,9 +253,39 @@ public function testExtensionWithoutFields() : void } /** - * @see it('Extension without fields followed by extension') + * @see it('Interface extension without fields') */ - public function testExtensionWithoutFieldsFollowedByExtension() : void + public function testInterfaceExtensionWithoutFields() : void + { + $body = 'extend interface Hello implements Greeting'; + $doc = Parser::parse($body); + $loc = static function ($start, $end) : array { + return TestUtils::locArray($start, $end); + }; + + $expected = [ + 'kind' => NodeKind::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKind::INTERFACE_TYPE_EXTENSION, + 'name' => $this->nameNode('Hello', $loc(17, 22)), + 'interfaces' => [ + $this->typeNode('Greeting', $loc(34, 42)), + ], + 'directives' => [], + 'fields' => [], + 'loc' => $loc(0, 42), + ], + ], + 'loc' => $loc(0, 42), + ]; + self::assertEquals($expected, TestUtils::nodeToArray($doc)); + } + + /** + * @see it('Object extension without fields followed by extension') + */ + public function testObjectExtensionWithoutFieldsFollowedByExtension() : void { $body = ' extend type Hello implements Greeting @@ -261,7 +297,7 @@ public function testExtensionWithoutFieldsFollowedByExtension() : void 'kind' => 'Document', 'definitions' => [ [ - 'kind' => 'ObjectTypeExtension', + 'kind' => NodeKind::OBJECT_TYPE_EXTENSION, 'name' => $this->nameNode('Hello', ['start' => 23, 'end' => 28]), 'interfaces' => [$this->typeNode('Greeting', ['start' => 40, 'end' => 48])], 'directives' => [], @@ -269,7 +305,7 @@ public function testExtensionWithoutFieldsFollowedByExtension() : void 'loc' => ['start' => 11, 'end' => 48], ], [ - 'kind' => 'ObjectTypeExtension', + 'kind' => NodeKind::OBJECT_TYPE_EXTENSION, 'name' => $this->nameNode('Hello', ['start' => 76, 'end' => 81]), 'interfaces' => [$this->typeNode('SecondGreeting', ['start' => 93, 'end' => 107])], 'directives' => [], @@ -283,9 +319,45 @@ public function testExtensionWithoutFieldsFollowedByExtension() : void } /** - * @see it('Extension without anything throws') + * @see it('Interface extension without fields followed by extension') */ - public function testExtensionWithoutAnythingThrows() : void + public function testInterfaceExtensionWithoutFieldsFollowedByExtension() : void + { + $body = ' + extend interface Hello implements Greeting + + extend interface Hello implements SecondGreeting + '; + $doc = Parser::parse($body); + $expected = [ + 'kind' => 'Document', + 'definitions' => [ + [ + 'kind' => NodeKind::INTERFACE_TYPE_EXTENSION, + 'name' => $this->nameNode('Hello', ['start' => 28, 'end' => 33]), + 'interfaces' => [$this->typeNode('Greeting', ['start' => 45, 'end' => 53])], + 'directives' => [], + 'fields' => [], + 'loc' => ['start' => 11, 'end' => 53], + ], + [ + 'kind' => NodeKind::INTERFACE_TYPE_EXTENSION, + 'name' => $this->nameNode('Hello', ['start' => 82, 'end' => 87]), + 'interfaces' => [$this->typeNode('SecondGreeting', ['start' => 99, 'end' => 113])], + 'directives' => [], + 'fields' => [], + 'loc' => ['start' => 65, 'end' => 113], + ], + ], + 'loc' => ['start' => 0, 'end' => 122], + ]; + self::assertEquals($expected, $doc->toArray(true)); + } + + /** + * @see it('Object extension without anything throws') + */ + public function testObjectExtensionWithoutAnythingThrows() : void { $this->expectSyntaxError( 'extend type Hello', @@ -294,6 +366,18 @@ public function testExtensionWithoutAnythingThrows() : void ); } + /** + * @see it('Interface extension without anything throws') + */ + public function testInterfaceExtensionWithoutAnythingThrows() : void + { + $this->expectSyntaxError( + 'extend interface Hello', + 'Unexpected ', + $this->loc(1, 23) + ); + } + private function expectSyntaxError($text, $message, $location) { $this->expectException(SyntaxError::class); @@ -312,9 +396,9 @@ private function loc($line, $column) } /** - * @see it('Extension do not include descriptions') + * @see it('Object extension do not include descriptions') */ - public function testExtensionDoNotIncludeDescriptions() : void + public function testObjectExtensionDoNotIncludeDescriptions() : void { $body = ' "Description" @@ -329,9 +413,26 @@ public function testExtensionDoNotIncludeDescriptions() : void } /** - * @see it('Extension do not include descriptions') + * @see it('Interface extension do not include descriptions') */ - public function testExtensionDoNotIncludeDescriptions2() : void + public function testInterfaceExtensionDoNotIncludeDescriptions() : void + { + $body = ' + "Description" + extend interface Hello { + world: String + }'; + $this->expectSyntaxError( + $body, + 'Unexpected Name "extend"', + $this->loc(3, 7) + ); + } + + /** + * @see it('Object Extension do not include descriptions') + */ + public function testObjectExtensionDoNotIncludeDescriptions2() : void { $body = ' extend "Description" type Hello { @@ -345,6 +446,23 @@ public function testExtensionDoNotIncludeDescriptions2() : void ); } + /** + * @see it('Interface Extension do not include descriptions') + */ + public function testInterfaceExtensionDoNotIncludeDescriptions2() : void + { + $body = ' + extend "Description" interface Hello { + world: String + } +}'; + $this->expectSyntaxError( + $body, + 'Unexpected String "Description"', + $this->loc(2, 14) + ); + } + /** * @see it('Simple non-null type') */ @@ -356,7 +474,7 @@ public function testSimpleNonNullType() : void }'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -389,6 +507,44 @@ public function testSimpleNonNullType() : void self::assertEquals($expected, TestUtils::nodeToArray($doc)); } + /** + * @see it('Simple interface inheriting interface') + */ + public function testSimpleInterfaceInheritingInterface() : void + { + $body = 'interface Hello implements World { field: String }'; + $doc = Parser::parse($body); + $loc = static function ($start, $end) : array { + return TestUtils::locArray($start, $end); + }; + + $expected = [ + 'kind' => NodeKind::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKind::INTERFACE_TYPE_DEFINITION, + 'name' => $this->nameNode('Hello', $loc(10, 15)), + 'interfaces' => [ + $this->typeNode('World', $loc(27, 32)), + ], + 'directives' => [], + 'fields' => [ + $this->fieldNode( + $this->nameNode('field', $loc(35, 40)), + $this->typeNode('String', $loc(42, 48)), + $loc(35, 48) + ), + ], + 'loc' => $loc(0, 50), + 'description' => null, + ], + ], + 'loc' => $loc(0, 50), + ]; + + self::assertEquals($expected, TestUtils::nodeToArray($doc)); + } + /** * @see it('Simple type inheriting interface') */ @@ -396,7 +552,7 @@ public function testSimpleTypeInheritingInterface() : void { $body = 'type Hello implements World { field: String }'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -434,7 +590,7 @@ public function testSimpleTypeInheritingMultipleInterfaces() : void { $body = 'type Hello implements Wo & rld { field: String }'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -466,6 +622,45 @@ public function testSimpleTypeInheritingMultipleInterfaces() : void self::assertEquals($expected, TestUtils::nodeToArray($doc)); } + /** + * @see it('Simple interface inheriting multiple interfaces') + */ + public function testSimpleInterfaceInheritingMultipleInterfaces() : void + { + $body = 'interface Hello implements Wo & rld { field: String }'; + $doc = Parser::parse($body); + $loc = static function ($start, $end) : array { + return TestUtils::locArray($start, $end); + }; + + $expected = [ + 'kind' => NodeKind::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKind::INTERFACE_TYPE_DEFINITION, + 'name' => $this->nameNode('Hello', $loc(10, 15)), + 'interfaces' => [ + $this->typeNode('Wo', $loc(27, 29)), + $this->typeNode('rld', $loc(32, 35)), + ], + 'directives' => [], + 'fields' => [ + $this->fieldNode( + $this->nameNode('field', $loc(38, 43)), + $this->typeNode('String', $loc(45, 51)), + $loc(38, 51) + ), + ], + 'loc' => $loc(0, 53), + 'description' => null, + ], + ], + 'loc' => $loc(0, 53), + ]; + + self::assertEquals($expected, TestUtils::nodeToArray($doc)); + } + /** * @see it('Simple type inheriting multiple interfaces with leading ampersand') */ @@ -473,7 +668,7 @@ public function testSimpleTypeInheritingMultipleInterfacesWithLeadingAmpersand() { $body = 'type Hello implements & Wo & rld { field: String }'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -481,7 +676,7 @@ public function testSimpleTypeInheritingMultipleInterfacesWithLeadingAmpersand() 'kind' => 'Document', 'definitions' => [ [ - 'kind' => 'ObjectTypeDefinition', + 'kind' => NodeKind::OBJECT_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(5, 10)), 'interfaces' => [ $this->typeNode('Wo', $loc(24, 26)), @@ -504,6 +699,44 @@ public function testSimpleTypeInheritingMultipleInterfacesWithLeadingAmpersand() self::assertEquals($expected, TestUtils::nodeToArray($doc)); } + /** + * @see it('Simple interface inheriting multiple interfaces with leading ampersand') + */ + public function testSimpleInterfaceInheritingMultipleInterfacesWithLeadingAmpersand() : void + { + $body = 'interface Hello implements & Wo & rld { field: String }'; + $doc = Parser::parse($body); + $loc = static function ($start, $end) : array { + return TestUtils::locArray($start, $end); + }; + + $expected = [ + 'kind' => 'Document', + 'definitions' => [ + [ + 'kind' => NodeKind::INTERFACE_TYPE_DEFINITION, + 'name' => $this->nameNode('Hello', $loc(10, 15)), + 'interfaces' => [ + $this->typeNode('Wo', $loc(29, 31)), + $this->typeNode('rld', $loc(34, 37)), + ], + 'directives' => [], + 'fields' => [ + $this->fieldNode( + $this->nameNode('field', $loc(40, 45)), + $this->typeNode('String', $loc(47, 53)), + $loc(40, 53) + ), + ], + 'loc' => $loc(0, 55), + 'description' => null, + ], + ], + 'loc' => $loc(0, 55), + ]; + self::assertEquals($expected, TestUtils::nodeToArray($doc)); + } + /** * @see it('Single value enum') */ @@ -511,7 +744,7 @@ public function testSingleValueEnum() : void { $body = 'enum Hello { WORLD }'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -551,7 +784,7 @@ public function testDoubleValueEnum() : void { $body = 'enum Hello { WO, RLD }'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -586,7 +819,7 @@ interface Hello { world: String }'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -604,6 +837,7 @@ interface Hello { $loc(21, 34) ), ], + 'interfaces' => [], 'loc' => $loc(1, 36), 'description' => null, ], @@ -623,7 +857,7 @@ public function testSimpleFieldWithArg() : void world(flag: Boolean): String }'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -683,7 +917,7 @@ public function testSimpleFieldWithArgWithDefaultValue() : void world(flag: Boolean = true): String }'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -729,7 +963,7 @@ public function testSimpleFieldWithListArg() : void world(things: [String]): String }'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -782,7 +1016,7 @@ public function testSimpleFieldWithTwoArgs() : void world(argOne: Boolean, argTwo: Int): String }'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -832,7 +1066,7 @@ public function testSimpleUnion() : void { $body = 'union Hello = World'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -861,7 +1095,7 @@ public function testUnionWithTwoTypes() : void { $body = 'union Hello = Wo | Rld'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -967,7 +1201,7 @@ public function testScalar() : void { $body = 'scalar Hello'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -997,7 +1231,7 @@ public function testSimpleInputObject() : void world: String }'; $doc = Parser::parse($body); - $loc = static function ($start, $end) { + $loc = static function ($start, $end) : array { return TestUtils::locArray($start, $end); }; @@ -1041,6 +1275,86 @@ public function testSimpleInputObjectWithArgsShouldFail() : void ); } + /** + * @see it('Directive definition', () => { + */ + public function testDirectiveDefinition() : void + { + $body = 'directive @foo on OBJECT | INTERFACE'; + $doc = Parser::parse($body); + $loc = static function ($start, $end) : array { + return TestUtils::locArray($start, $end); + }; + + $expected = [ + 'kind' => NodeKind::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKind::DIRECTIVE_DEFINITION, + 'name' => $this->nameNode('foo', $loc(11, 14)), + 'description' => null, + 'arguments' => [], + 'repeatable' => false, + 'locations' => [ + [ + 'kind' => NodeKind::NAME, + 'value' => DirectiveLocation::OBJECT, + 'loc' => $loc(18, 24), + ], + [ + 'kind' => NodeKind::NAME, + 'value' => DirectiveLocation::IFACE, + 'loc' => $loc(27, 36), + ], + ], + 'loc' => $loc(0, 36), + ], + ], + 'loc' => $loc(0, 36), + ]; + self::assertEquals($expected, TestUtils::nodeToArray($doc)); + } + + /** + * @see it('Repeatable directive definition', () => { + */ + public function testRepeatableDirectiveDefinition() : void + { + $body = 'directive @foo repeatable on OBJECT | INTERFACE'; + $doc = Parser::parse($body); + $loc = static function ($start, $end) : array { + return TestUtils::locArray($start, $end); + }; + + $expected = [ + 'kind' => NodeKind::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKind::DIRECTIVE_DEFINITION, + 'name' => $this->nameNode('foo', $loc(11, 14)), + 'description' => null, + 'arguments' => [], + 'repeatable' => true, + 'locations' => [ + [ + 'kind' => NodeKind::NAME, + 'value' => DirectiveLocation::OBJECT, + 'loc' => $loc(29, 35), + ], + [ + 'kind' => NodeKind::NAME, + 'value' => DirectiveLocation::IFACE, + 'loc' => $loc(38, 47), + ], + ], + 'loc' => $loc(0, 47), + ], + ], + 'loc' => $loc(0, 47), + ]; + self::assertEquals($expected, TestUtils::nodeToArray($doc)); + } + /** * @see it('Directive with incorrect locations') */ diff --git a/tests/Language/SchemaPrinterTest.php b/tests/Language/SchemaPrinterTest.php index b5769835a..0c62e337d 100644 --- a/tests/Language/SchemaPrinterTest.php +++ b/tests/Language/SchemaPrinterTest.php @@ -71,7 +71,7 @@ public function testPrintsKitchenSink() : void This is a description of the `Foo` type. """ -type Foo implements Bar & Baz { +type Foo implements Bar & Baz & Two { one: Type """ This is a description of the `two` field. @@ -112,12 +112,18 @@ interface AnnotatedInterface @onInterface { interface UndefinedInterface -extend interface Bar { +extend interface Bar implements Two { two(argument: InputType!): Type } extend interface Bar @onInterface +interface Baz implements Bar & Two { + one: Type + two(argument: InputType!): Type + four(argument: String = "string"): String +} + union Feed = Story | Article | Advert union AnnotatedUnion @onUnion = A | B @@ -176,6 +182,8 @@ enum UndefinedEnum directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT directive @include2(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @myRepeatableDir(name: String!) repeatable on OBJECT | INTERFACE '; self::assertEquals($expected, $printed); } diff --git a/tests/Language/VisitorTest.php b/tests/Language/VisitorTest.php index 4a0432674..04cd7476c 100644 --- a/tests/Language/VisitorTest.php +++ b/tests/Language/VisitorTest.php @@ -4,6 +4,7 @@ namespace GraphQL\Tests\Language; +use GraphQL\Language\AST\DefinitionNode; use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\FieldNode; use GraphQL\Language\AST\NameNode; @@ -15,20 +16,31 @@ use GraphQL\Language\Parser; use GraphQL\Language\Printer; use GraphQL\Language\Visitor; +use GraphQL\Language\VisitorOperation; use GraphQL\Tests\Validator\ValidatorTestCase; +use GraphQL\Type\Definition\EnumType; +use GraphQL\Type\Definition\InputObjectType; +use GraphQL\Type\Definition\ListOfType; +use GraphQL\Type\Definition\NonNull; +use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Utils\TypeInfo; use function array_keys; +use function array_pop; use function array_slice; use function count; use function file_get_contents; use function func_get_args; use function gettype; use function is_array; +use function is_numeric; use function iterator_to_array; class VisitorTest extends ValidatorTestCase { + /** + * @see it('validates path argument') + */ public function testValidatesPathArgument() : void { $visited = []; @@ -38,11 +50,11 @@ public function testValidatesPathArgument() : void Visitor::visit( $ast, [ - 'enter' => function ($node, $key, $parent, $path) use ($ast, &$visited) { + 'enter' => function ($node, $key, $parent, $path) use ($ast, &$visited) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['enter', $path]; }, - 'leave' => function ($node, $key, $parent, $path) use ($ast, &$visited) { + 'leave' => function ($node, $key, $parent, $path) use ($ast, &$visited) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['leave', $path]; }, @@ -65,12 +77,49 @@ public function testValidatesPathArgument() : void self::assertEquals($expected, $visited); } + /** + * @see it('validates ancestors argument') + */ + public function testValidatesAncestorsArgument() + { + $ast = Parser::parse('{ a }', ['noLocation' => true]); + $visitedNodes = []; + + Visitor::visit($ast, [ + 'enter' => static function ($node, $key, $parent, $path, $ancestors) use (&$visitedNodes) : void { + $inArray = is_numeric($key); + if ($inArray) { + $visitedNodes[] = $parent; + } + $visitedNodes[] = $node; + + $expectedAncestors = array_slice($visitedNodes, 0, -2); + self::assertEquals($expectedAncestors, $ancestors); + }, + 'leave' => static function ($node, $key, $parent, $path, $ancestors) use (&$visitedNodes) : void { + $expectedAncestors = array_slice($visitedNodes, 0, -2); + self::assertEquals($expectedAncestors, $ancestors); + + $inArray = is_numeric($key); + if ($inArray) { + array_pop($visitedNodes); + } + array_pop($visitedNodes); + }, + ]); + } + private function checkVisitorFnArgs($ast, $args, $isEdited = false) { /** @var Node $node */ [$node, $key, $parent, $path, $ancestors] = $args; - $parentArray = $parent && ! is_array($parent) ? ($parent instanceof NodeList ? iterator_to_array($parent) : $parent->toArray()) : $parent; + $parentArray = $parent && ! is_array($parent) + ? ($parent instanceof NodeList + ? iterator_to_array($parent) + : $parent->toArray() + ) + : $parent; self::assertInstanceOf(Node::class, $node); self::assertContains($node->kind, array_keys(NodeKind::$classMap)); @@ -91,10 +140,10 @@ private function checkVisitorFnArgs($ast, $args, $isEdited = false) self::assertArrayHasKey($key, $parentArray); - self::assertInternalType('array', $path); + self::assertIsArray($path); self::assertEquals($key, $path[count($path) - 1]); - self::assertInternalType('array', $ancestors); + self::assertIsArray($ancestors); self::assertCount(count($path) - 1, $ancestors); if ($isEdited) { @@ -114,7 +163,9 @@ private function getNodeByPath(DocumentNode $ast, $path) { $result = $ast; foreach ($path as $key) { - $resultArray = $result instanceof NodeList ? iterator_to_array($result) : $result->toArray(); + $resultArray = $result instanceof NodeList + ? iterator_to_array($result) + : $result->toArray(); self::assertArrayHasKey($key, $resultArray); $result = $resultArray[$key]; } @@ -131,7 +182,7 @@ public function testAllowsEditingNodeOnEnterAndOnLeave() : void $ast, [ NodeKind::OPERATION_DEFINITION => [ - 'enter' => function (OperationDefinitionNode $node) use (&$selectionSet, $ast) { + 'enter' => function (OperationDefinitionNode $node) use (&$selectionSet, $ast) : OperationDefinitionNode { $this->checkVisitorFnArgs($ast, func_get_args()); $selectionSet = $node->selectionSet; @@ -143,7 +194,7 @@ public function testAllowsEditingNodeOnEnterAndOnLeave() : void return $newNode; }, - 'leave' => function (OperationDefinitionNode $node) use (&$selectionSet, $ast) { + 'leave' => function (OperationDefinitionNode $node) use (&$selectionSet, $ast) : OperationDefinitionNode { $this->checkVisitorFnArgs($ast, func_get_args(), true); $newNode = clone $node; $newNode->selectionSet = $selectionSet; @@ -157,6 +208,7 @@ public function testAllowsEditingNodeOnEnterAndOnLeave() : void self::assertNotEquals($ast, $editedAst); + /** @var DocumentNode $expected */ $expected = $ast->cloneDeep(); $expected->definitions[0]->didEnter = true; $expected->definitions[0]->didLeave = true; @@ -173,15 +225,17 @@ public function testAllowsEditingRootNodeOnEnterAndLeave() : void $ast, [ NodeKind::DOCUMENT => [ - 'enter' => function (DocumentNode $node) use ($ast) { + 'enter' => function (DocumentNode $node) use ($ast) : DocumentNode { $this->checkVisitorFnArgs($ast, func_get_args()); - $tmp = clone $node; - $tmp->definitions = []; - $tmp->didEnter = true; + /** @var NodeList $definitionNodeList */ + $definitionNodeList = new NodeList([]); + $tmp = clone $node; + $tmp->definitions = $definitionNodeList; + $tmp->didEnter = true; return $tmp; }, - 'leave' => function (DocumentNode $node) use ($definitions, $ast) { + 'leave' => function (DocumentNode $node) use ($definitions, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args(), true); $node->definitions = $definitions; $node->didLeave = true; @@ -205,11 +259,13 @@ public function testAllowsForEditingOnEnter() : void $editedAst = Visitor::visit( $ast, [ - 'enter' => function ($node) use ($ast) { + 'enter' => function ($node) use ($ast) : ?VisitorOperation { $this->checkVisitorFnArgs($ast, func_get_args()); if ($node instanceof FieldNode && $node->name->value === 'b') { return Visitor::removeNode(); } + + return null; }, ] ); @@ -230,11 +286,13 @@ public function testAllowsForEditingOnLeave() : void $editedAst = Visitor::visit( $ast, [ - 'leave' => function ($node) use ($ast) { + 'leave' => function ($node) use ($ast) : ?VisitorOperation { $this->checkVisitorFnArgs($ast, func_get_args(), true); if ($node instanceof FieldNode && $node->name->value === 'b') { return Visitor::removeNode(); } + + return null; }, ] ); @@ -263,7 +321,7 @@ public function testVisitsEditedNode() : void Visitor::visit( $ast, [ - 'enter' => function ($node) use ($addedField, &$didVisitAddedField, $ast) { + 'enter' => function ($node) use ($addedField, &$didVisitAddedField, $ast) : ?FieldNode { $this->checkVisitorFnArgs($ast, func_get_args(), true); if ($node instanceof FieldNode && $node->name->value === 'a') { return new FieldNode([ @@ -273,10 +331,12 @@ public function testVisitsEditedNode() : void ]); } if ($node !== $addedField) { - return; + return null; } $didVisitAddedField = true; + + return null; }, ] ); @@ -292,14 +352,16 @@ public function testAllowsSkippingASubTree() : void Visitor::visit( $ast, [ - 'enter' => function (Node $node) use (&$visited, $ast) { + 'enter' => function (Node $node) use (&$visited, $ast) : ?VisitorOperation { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['enter', $node->kind, $node->value ?? null]; if ($node instanceof FieldNode && $node->name->value === 'b') { return Visitor::skipNode(); } + + return null; }, - 'leave' => function (Node $node) use (&$visited, $ast) { + 'leave' => function (Node $node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['leave', $node->kind, $node->value ?? null]; }, @@ -335,14 +397,16 @@ public function testAllowsEarlyExitWhileVisiting() : void Visitor::visit( $ast, [ - 'enter' => function (Node $node) use (&$visited, $ast) { + 'enter' => function (Node $node) use (&$visited, $ast) : ?VisitorOperation { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['enter', $node->kind, $node->value ?? null]; if ($node instanceof NameNode && $node->value === 'x') { return Visitor::stop(); } + + return null; }, - 'leave' => function (Node $node) use (&$visited, $ast) { + 'leave' => function (Node $node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['leave', $node->kind, $node->value ?? null]; }, @@ -376,17 +440,19 @@ public function testAllowsEarlyExitWhileLeaving() : void Visitor::visit( $ast, [ - 'enter' => function ($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['enter', $node->kind, $node->value ?? null]; }, - 'leave' => function ($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) : ?VisitorOperation { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['leave', $node->kind, $node->value ?? null]; - if ($node->kind === NodeKind::NAME && $node->value === 'x') { + if ($node instanceof NameNode && $node->value === 'x') { return Visitor::stop(); } + + return null; }, ] ); @@ -420,16 +486,16 @@ public function testAllowsANamedFunctionsVisitorAPI() : void Visitor::visit( $ast, [ - NodeKind::NAME => function (NameNode $node) use (&$visited, $ast) { + NodeKind::NAME => function (NameNode $node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['enter', $node->kind, $node->value]; }, NodeKind::SELECTION_SET => [ - 'enter' => function (SelectionSetNode $node) use (&$visited, $ast) { + 'enter' => function (SelectionSetNode $node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['enter', $node->kind, null]; }, - 'leave' => function (SelectionSetNode $node) use (&$visited, $ast) { + 'leave' => function (SelectionSetNode $node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['leave', $node->kind, null]; }, @@ -465,11 +531,11 @@ public function testExperimentalVisitsVariablesDefinedInFragments() : void Visitor::visit( $ast, [ - 'enter' => function ($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['enter', $node->kind, $node->value ?? null]; }, - 'leave' => function ($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['leave', $node->kind, $node->value ?? null]; }, @@ -519,12 +585,12 @@ public function testVisitsKitchenSink() : void Visitor::visit( $ast, [ - 'enter' => function (Node $node, $key, $parent) use (&$visited, $ast) { + 'enter' => function (Node $node, $key, $parent) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $r = ['enter', $node->kind, $key, $parent instanceof Node ? $parent->kind : null]; $visited[] = $r; }, - 'leave' => function (Node $node, $key, $parent) use (&$visited, $ast) { + 'leave' => function (Node $node, $key, $parent) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $r = ['leave', $node->kind, $key, $parent instanceof Node ? $parent->kind : null]; $visited[] = $r; @@ -861,16 +927,18 @@ public function testAllowsSkippingSubTree() : void $ast, Visitor::visitInParallel([ [ - 'enter' => function ($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) : ?VisitorOperation { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['enter', $node->kind, $node->value ?? null]; if ($node->kind === 'Field' && isset($node->name->value) && $node->name->value === 'b') { return Visitor::skipNode(); } + + return null; }, - 'leave' => function ($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['leave', $node->kind, $node->value ?? null]; }, @@ -909,27 +977,31 @@ public function testAllowsSkippingDifferentSubTrees() : void $ast, Visitor::visitInParallel([ [ - 'enter' => function ($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) : ?VisitorOperation { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['no-a', 'enter', $node->kind, $node->value ?? null]; if ($node->kind === 'Field' && isset($node->name->value) && $node->name->value === 'a') { return Visitor::skipNode(); } + + return null; }, - 'leave' => function ($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['no-a', 'leave', $node->kind, $node->value ?? null]; }, ], [ - 'enter' => function ($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) : ?VisitorOperation { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['no-b', 'enter', $node->kind, $node->value ?? null]; if ($node->kind === 'Field' && isset($node->name->value) && $node->name->value === 'b') { return Visitor::skipNode(); } + + return null; }, - 'leave' => function ($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['no-b', 'leave', $node->kind, $node->value ?? null]; }, @@ -986,15 +1058,17 @@ public function testAllowsEarlyExitWhileVisiting2() : void Visitor::visit( $ast, Visitor::visitInParallel([[ - 'enter' => function ($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) : ?VisitorOperation { $this->checkVisitorFnArgs($ast, func_get_args()); $value = $node->value ?? null; $visited[] = ['enter', $node->kind, $value]; if ($node->kind === 'Name' && $value === 'x') { return Visitor::stop(); } + + return null; }, - 'leave' => function ($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['leave', $node->kind, $node->value ?? null]; }, @@ -1031,29 +1105,33 @@ public function testAllowsEarlyExitFromDifferentPoints() : void $ast, Visitor::visitInParallel([ [ - 'enter' => function ($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) : ?VisitorOperation { $this->checkVisitorFnArgs($ast, func_get_args()); $value = $node->value ?? null; $visited[] = ['break-a', 'enter', $node->kind, $value]; if ($node->kind === 'Name' && $value === 'a') { return Visitor::stop(); } + + return null; }, - 'leave' => function ($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['break-a', 'leave', $node->kind, $node->value ?? null]; }, ], [ - 'enter' => function ($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) : ?VisitorOperation { $this->checkVisitorFnArgs($ast, func_get_args()); $value = $node->value ?? null; $visited[] = ['break-b', 'enter', $node->kind, $value]; if ($node->kind === 'Name' && $value === 'b') { return Visitor::stop(); } + + return null; }, - 'leave' => function ($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['break-b', 'leave', $node->kind, $node->value ?? null]; }, @@ -1096,17 +1174,19 @@ public function testAllowsEarlyExitWhileLeaving2() : void Visitor::visit( $ast, Visitor::visitInParallel([[ - 'enter' => function ($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['enter', $node->kind, $node->value ?? null]; }, - 'leave' => function ($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) : ?VisitorOperation { $this->checkVisitorFnArgs($ast, func_get_args()); $value = $node->value ?? null; $visited[] = ['leave', $node->kind, $value]; if ($node->kind === 'Name' && $value === 'x') { return Visitor::stop(); } + + return null; }, ], ]) @@ -1142,29 +1222,33 @@ public function testAllowsEarlyExitFromLeavingDifferentPoints() : void $ast, Visitor::visitInParallel([ [ - 'enter' => function ($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['break-a', 'enter', $node->kind, $node->value ?? null]; }, - 'leave' => function ($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) : ?VisitorOperation { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['break-a', 'leave', $node->kind, $node->value ?? null]; if ($node->kind === 'Field' && isset($node->name->value) && $node->name->value === 'a') { return Visitor::stop(); } + + return null; }, ], [ - 'enter' => function ($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['break-b', 'enter', $node->kind, $node->value ?? null]; }, - 'leave' => function ($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) : ?VisitorOperation { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['break-b', 'leave', $node->kind, $node->value ?? null]; if ($node->kind === 'Field' && isset($node->name->value) && $node->name->value === 'b') { return Visitor::stop(); } + + return null; }, ], ]) @@ -1222,19 +1306,21 @@ public function testAllowsForEditingOnEnter2() : void $ast, Visitor::visitInParallel([ [ - 'enter' => function ($node) use ($ast) { + 'enter' => function ($node) use ($ast) : ?VisitorOperation { $this->checkVisitorFnArgs($ast, func_get_args()); if ($node->kind === 'Field' && isset($node->name->value) && $node->name->value === 'b') { return Visitor::removeNode(); } + + return null; }, ], [ - 'enter' => function ($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['enter', $node->kind, $node->value ?? null]; }, - 'leave' => function ($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args(), true); $visited[] = ['leave', $node->kind, $node->value ?? null]; }, @@ -1292,19 +1378,21 @@ public function testAllowsForEditingOnLeave2() : void $ast, Visitor::visitInParallel([ [ - 'leave' => function ($node) use ($ast) { + 'leave' => function ($node) use ($ast) : ?VisitorOperation { $this->checkVisitorFnArgs($ast, func_get_args(), true); if ($node->kind === 'Field' && isset($node->name->value) && $node->name->value === 'b') { return Visitor::removeNode(); } + + return null; }, ], [ - 'enter' => function ($node) use (&$visited, $ast) { + 'enter' => function ($node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $visited[] = ['enter', $node->kind, $node->value ?? null]; }, - 'leave' => function ($node) use (&$visited, $ast) { + 'leave' => function ($node) use (&$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args(), true); $visited[] = ['leave', $node->kind, $node->value ?? null]; }, @@ -1374,12 +1462,13 @@ public function testMaintainsTypeInfoDuringVisit() : void Visitor::visitWithTypeInfo( $typeInfo, [ - 'enter' => function ($node) use ($typeInfo, &$visited, $ast) { + 'enter' => function ($node) use ($typeInfo, &$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $parentType = $typeInfo->getParentType(); $type = $typeInfo->getType(); - $inputType = $typeInfo->getInputType(); - $visited[] = [ + /** @var ScalarType|EnumType|InputObjectType|ListOfType|NonNull|null $inputType */ + $inputType = $typeInfo->getInputType(); + $visited[] = [ 'enter', $node->kind, $node->kind === 'Name' ? $node->value : null, @@ -1388,12 +1477,13 @@ public function testMaintainsTypeInfoDuringVisit() : void $inputType ? (string) $inputType : null, ]; }, - 'leave' => function ($node) use ($typeInfo, &$visited, $ast) { + 'leave' => function ($node) use ($typeInfo, &$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args()); $parentType = $typeInfo->getParentType(); $type = $typeInfo->getType(); - $inputType = $typeInfo->getInputType(); - $visited[] = [ + /** @var ScalarType|EnumType|InputObjectType|ListOfType|NonNull|null $inputType */ + $inputType = $typeInfo->getInputType(); + $visited[] = [ 'leave', $node->kind, $node->kind === 'Name' ? $node->value : null, @@ -1466,12 +1556,13 @@ public function testMaintainsTypeInfoDuringEdit() : void Visitor::visitWithTypeInfo( $typeInfo, [ - 'enter' => function ($node) use ($typeInfo, &$visited, $ast) { + 'enter' => function ($node) use ($typeInfo, &$visited, $ast) : ?FieldNode { $this->checkVisitorFnArgs($ast, func_get_args(), true); $parentType = $typeInfo->getParentType(); $type = $typeInfo->getType(); - $inputType = $typeInfo->getInputType(); - $visited[] = [ + /** @var ScalarType|EnumType|InputObjectType|ListOfType|NonNull|null $inputType */ + $inputType = $typeInfo->getInputType(); + $visited[] = [ 'enter', $node->kind, $node->kind === 'Name' ? $node->value : null, @@ -1499,13 +1590,16 @@ public function testMaintainsTypeInfoDuringEdit() : void ]), ]); } + + return null; }, - 'leave' => function ($node) use ($typeInfo, &$visited, $ast) { + 'leave' => function ($node) use ($typeInfo, &$visited, $ast) : void { $this->checkVisitorFnArgs($ast, func_get_args(), true); $parentType = $typeInfo->getParentType(); $type = $typeInfo->getType(); - $inputType = $typeInfo->getInputType(); - $visited[] = [ + /** @var ScalarType|EnumType|InputObjectType|ListOfType|NonNull|null $inputType */ + $inputType = $typeInfo->getInputType(); + $visited[] = [ 'leave', $node->kind, $node->kind === 'Name' ? $node->value : null, diff --git a/tests/Language/kitchen-sink-noloc.ast b/tests/Language/kitchen-sink-noloc.ast index d1427c12b..980a95d24 100644 --- a/tests/Language/kitchen-sink-noloc.ast +++ b/tests/Language/kitchen-sink-noloc.ast @@ -24,7 +24,8 @@ "kind": "Name", "value": "ComplexType" } - } + }, + "directives": [] }, { "kind": "VariableDefinition", @@ -45,7 +46,8 @@ "defaultValue": { "kind": "EnumValue", "value": "MOBILE" - } + }, + "directives": [] } ], "directives": [], @@ -392,7 +394,8 @@ "kind": "Name", "value": "StoryLikeSubscribeInput" } - } + }, + "directives": [] } ], "directives": [], diff --git a/tests/Language/kitchen-sink.ast b/tests/Language/kitchen-sink.ast index 606b03c25..ffc35480e 100644 --- a/tests/Language/kitchen-sink.ast +++ b/tests/Language/kitchen-sink.ast @@ -56,7 +56,8 @@ }, "value": "ComplexType" } - } + }, + "directives": [] }, { "kind": "VariableDefinition", @@ -101,7 +102,8 @@ "end": 343 }, "value": "MOBILE" - } + }, + "directives": [] } ], "directives": [], @@ -772,7 +774,8 @@ }, "value": "StoryLikeSubscribeInput" } - } + }, + "directives": [] } ], "directives": [], diff --git a/tests/Language/schema-kitchen-sink.graphql b/tests/Language/schema-kitchen-sink.graphql index 3ad830cbf..2b0f54f11 100644 --- a/tests/Language/schema-kitchen-sink.graphql +++ b/tests/Language/schema-kitchen-sink.graphql @@ -12,7 +12,7 @@ schema { This is a description of the `Foo` type. """ -type Foo implements Bar & Baz { +type Foo implements Bar & Baz & Two { one: Type """ This is a description of the `two` field. @@ -53,12 +53,18 @@ interface AnnotatedInterface @onInterface { interface UndefinedInterface - extend interface Bar { +extend interface Bar implements Two{ two(argument: InputType!): Type } extend interface Bar @onInterface +interface Baz implements Bar & Two { + one: Type + two(argument: InputType!): Type + four(argument: String = "string"): String +} + union Feed = Story | Article | Advert union AnnotatedUnion @onUnion = A | B @@ -123,3 +129,7 @@ directive @include2(if: Boolean!) on | FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @myRepeatableDir(name: String!) repeatable on + | OBJECT + | INTERFACE diff --git a/tests/PHPUnit/ArraySubsetAsserts.php b/tests/PHPUnit/ArraySubsetAsserts.php new file mode 100644 index 000000000..caa9c028b --- /dev/null +++ b/tests/PHPUnit/ArraySubsetAsserts.php @@ -0,0 +1,41 @@ +strict = $strict; + $this->subset = $subset; + + if (method_exists(Constraint::class, '__construct')) { + parent::__construct(); + } + } + + /** + * Evaluates the constraint for parameter $other + * + * If $returnResult is set to false (the default), an exception is thrown + * in case of a failure. null is returned otherwise. + * + * If $returnResult is true, the result of the evaluation is returned as + * a boolean value instead: true in case of success, false in case of a + * failure. + * + * @param mixed[]|ArrayAccess $other + * @return mixed[]|null|bool + * + * @throws ExpectationFailedException + * @throws InvalidArgumentException + */ + public function evaluate($other, $description = '', $returnResult = false) + { + //type cast $other & $this->subset as an array to allow + //support in standard array functions. + $other = $this->toArray($other); + $this->subset = $this->toArray($this->subset); + $patched = array_replace_recursive($other, $this->subset); + if ($this->strict) { + $result = $other === $patched; + } else { + $result = $other == $patched; + } + if ($returnResult) { + return $result; + } + if ($result) { + return; + } + + $f = new ComparisonFailure( + $patched, + $other, + var_export($patched, true), + var_export($other, true) + ); + $this->fail($other, $description, $f); + } + + /** + * Returns a string representation of the constraint. + * + * @throws InvalidArgumentException + */ + public function toString(): string + { + $exporter = method_exists($this, 'exporter') ? $this->exporter() : $this->exporter; + + return 'has the subset ' . $exporter->export($this->subset); + } + + /** + * Returns the description of the failure + * + * The beginning of failure messages is "Failed asserting that" in most + * cases. This method should return the second part of that sentence. + * + * @param mixed $other evaluated value or object + * + * @throws InvalidArgumentException + */ + protected function failureDescription($other): string + { + return 'an array ' . $this->toString(); + } + + /** + * @param mixed[]|iterable $other + * + * @return mixed[] + */ + private function toArray(iterable $other): array + { + if (is_array($other)) { + return $other; + } + if ($other instanceof ArrayObject) { + return $other->getArrayCopy(); + } + if ($other instanceof Traversable) { + return iterator_to_array($other); + } + // Keep BC even if we know that array would not be the expected one + return (array) $other; + } +} diff --git a/tests/PhpStan/Type/Definition/Type/IsCompositeTypeStaticMethodTypeSpecifyingExtension.php b/tests/PhpStan/Type/Definition/Type/IsCompositeTypeStaticMethodTypeSpecifyingExtension.php new file mode 100644 index 000000000..3041bc942 --- /dev/null +++ b/tests/PhpStan/Type/Definition/Type/IsCompositeTypeStaticMethodTypeSpecifyingExtension.php @@ -0,0 +1,44 @@ +typeSpecifier = $typeSpecifier; + } + + public function isStaticMethodSupported(MethodReflection $staticMethodReflection, StaticCall $node, TypeSpecifierContext $context) : bool + { + // The $context argument tells us if we're in an if condition or not (as in this case). + return $staticMethodReflection->getName() === 'isCompositeType' && ! $context->null(); + } + + public function specifyTypes(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context) : SpecifiedTypes + { + return $this->typeSpecifier->create($node->args[0]->value, new ObjectType(CompositeType::class), $context); + } +} diff --git a/tests/PhpStan/Type/Definition/Type/IsInputTypeStaticMethodTypeSpecifyingExtension.php b/tests/PhpStan/Type/Definition/Type/IsInputTypeStaticMethodTypeSpecifyingExtension.php new file mode 100644 index 000000000..4b802ed13 --- /dev/null +++ b/tests/PhpStan/Type/Definition/Type/IsInputTypeStaticMethodTypeSpecifyingExtension.php @@ -0,0 +1,44 @@ +typeSpecifier = $typeSpecifier; + } + + public function isStaticMethodSupported(MethodReflection $staticMethodReflection, StaticCall $node, TypeSpecifierContext $context) : bool + { + // The $context argument tells us if we're in an if condition or not (as in this case). + return $staticMethodReflection->getName() === 'isInputType' && ! $context->null(); + } + + public function specifyTypes(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context) : SpecifiedTypes + { + return $this->typeSpecifier->create($node->args[0]->value, new ObjectType(InputType::class), $context); + } +} diff --git a/tests/PhpStan/Type/Definition/Type/IsOutputTypeStaticMethodTypeSpecifyingExtension.php b/tests/PhpStan/Type/Definition/Type/IsOutputTypeStaticMethodTypeSpecifyingExtension.php new file mode 100644 index 000000000..c348d21e7 --- /dev/null +++ b/tests/PhpStan/Type/Definition/Type/IsOutputTypeStaticMethodTypeSpecifyingExtension.php @@ -0,0 +1,44 @@ +typeSpecifier = $typeSpecifier; + } + + public function isStaticMethodSupported(MethodReflection $staticMethodReflection, StaticCall $node, TypeSpecifierContext $context) : bool + { + // The $context argument tells us if we're in an if condition or not (as in this case). + return $staticMethodReflection->getName() === 'isOutputType' && ! $context->null(); + } + + public function specifyTypes(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context) : SpecifiedTypes + { + return $this->typeSpecifier->create($node->args[0]->value, new ObjectType(OutputType::class), $context); + } +} diff --git a/tests/Regression/Issue396Test.php b/tests/Regression/Issue396Test.php new file mode 100644 index 000000000..33e494b07 --- /dev/null +++ b/tests/Regression/Issue396Test.php @@ -0,0 +1,160 @@ + 'A', 'fields' => ['name' => Type::string()]]); + $b = new ObjectType(['name' => 'B', 'fields' => ['name' => Type::string()]]); + $c = new ObjectType(['name' => 'C', 'fields' => ['name' => Type::string()]]); + + $log = []; + + $unionResult = new UnionType([ + 'name' => 'UnionResult', + 'types' => [$a, $b, $c], + 'resolveType' => static function ($result, $value, ResolveInfo $info) use ($a, $b, $c, &$log) : ?Type { + $log[] = [$result, $info->path]; + if (stristr($result['name'], 'A')) { + return $a; + } + if (stristr($result['name'], 'B')) { + return $b; + } + if (stristr($result['name'], 'C')) { + return $c; + } + + return null; + }, + ]); + + $exampleType = new ObjectType([ + 'name' => 'Example', + 'fields' => [ + 'field' => [ + 'type' => Type::nonNull(Type::listOf(Type::nonNull($unionResult))), + 'resolve' => static function () : array { + return [ + ['name' => 'A 1'], + ['name' => 'B 2'], + ['name' => 'C 3'], + ]; + }, + ], + ], + ]); + + $schema = new Schema(['query' => $exampleType]); + + $query = ' + query { + field { + ... on A { + name + } + ... on B { + name + } + ... on C { + name + } + } + } + '; + + GraphQL::executeQuery($schema, $query); + + $expected = [ + [['name' => 'A 1'], ['field', 0]], + [['name' => 'B 2'], ['field', 1]], + [['name' => 'C 3'], ['field', 2]], + ]; + self::assertEquals($expected, $log); + } + + public function testInterfaceResolveType() + { + $log = []; + + $interfaceResult = new InterfaceType([ + 'name' => 'InterfaceResult', + 'fields' => [ + 'name' => Type::string(), + ], + 'resolveType' => static function ($result, $value, ResolveInfo $info) use (&$a, &$b, &$c, &$log) : ?Type { + $log[] = [$result, $info->path]; + if (stristr($result['name'], 'A')) { + return $a; + } + if (stristr($result['name'], 'B')) { + return $b; + } + if (stristr($result['name'], 'C')) { + return $c; + } + + return null; + }, + ]); + + $a = new ObjectType(['name' => 'A', 'fields' => ['name' => Type::string()], 'interfaces' => [$interfaceResult]]); + $b = new ObjectType(['name' => 'B', 'fields' => ['name' => Type::string()], 'interfaces' => [$interfaceResult]]); + $c = new ObjectType(['name' => 'C', 'fields' => ['name' => Type::string()], 'interfaces' => [$interfaceResult]]); + + $exampleType = new ObjectType([ + 'name' => 'Example', + 'fields' => [ + 'field' => [ + 'type' => Type::nonNull(Type::listOf(Type::nonNull($interfaceResult))), + 'resolve' => static function () : array { + return [ + ['name' => 'A 1'], + ['name' => 'B 2'], + ['name' => 'C 3'], + ]; + }, + ], + ], + ]); + + $schema = new Schema([ + 'query' => $exampleType, + 'types' => [$a, $b, $c], + ]); + + $query = ' + query { + field { + name + } + } + '; + + GraphQL::executeQuery($schema, $query); + + $expected = [ + [['name' => 'A 1'], ['field', 0]], + [['name' => 'B 2'], ['field', 1]], + [['name' => 'C 3'], ['field', 2]], + ]; + self::assertEquals($expected, $log); + } +} diff --git a/tests/Regression/Issue467Test.php b/tests/Regression/Issue467Test.php new file mode 100644 index 000000000..988b4ac23 --- /dev/null +++ b/tests/Regression/Issue467Test.php @@ -0,0 +1,45 @@ + ['my message']]; + + $schema = BuildSchema::build($schemaStr); + $result = GraphQL::executeQuery($schema, $query, null, null, $variables); + + $expectedError = 'Variable "$msg" got invalid value ["my message"]; Field "0" is not defined by type MsgInput.'; + self::assertCount(1, $result->errors); + self::assertEquals($expectedError, $result->errors[0]->getMessage()); + } +} diff --git a/tests/Server/Psr7/PsrRequestStub.php b/tests/Server/Psr7/PsrRequestStub.php deleted file mode 100644 index c60dcac99..000000000 --- a/tests/Server/Psr7/PsrRequestStub.php +++ /dev/null @@ -1,610 +0,0 @@ -getHeaders() as $name => $values) { - * echo $name . ": " . implode(", ", $values); - * } - * - * // Emit headers iteratively: - * foreach ($message->getHeaders() as $name => $values) { - * foreach ($values as $value) { - * header(sprintf('%s: %s', $name, $value), false); - * } - * } - * - * While header names are not case-sensitive, getHeaders() will preserve the - * exact case in which headers were originally specified. - * - * @return string[][] Returns an associative array of the message's headers. Each - * key MUST be a header name, and each value MUST be an array of strings - * for that header. - */ - public function getHeaders() - { - throw new \Exception('Not implemented'); - } - - /** - * Checks if a header exists by the given case-insensitive name. - * - * @param string $name Case-insensitive header field name. - * @return bool Returns true if any header names match the given header - * name using a case-insensitive string comparison. Returns false if - * no matching header name is found in the message. - */ - public function hasHeader($name) - { - throw new \Exception('Not implemented'); - } - - /** - * Retrieves a message header value by the given case-insensitive name. - * - * This method returns an array of all the header values of the given - * case-insensitive header name. - * - * If the header does not appear in the message, this method MUST return an - * empty array. - * - * @param string $name Case-insensitive header field name. - * @return string[] An array of string values as provided for the given - * header. If the header does not appear in the message, this method MUST - * return an empty array. - */ - public function getHeader($name) - { - $name = strtolower($name); - - return $this->headers[$name] ?? []; - } - - /** - * Retrieves a comma-separated string of the values for a single header. - * - * This method returns all of the header values of the given - * case-insensitive header name as a string concatenated together using - * a comma. - * - * NOTE: Not all header values may be appropriately represented using - * comma concatenation. For such headers, use getHeader() instead - * and supply your own delimiter when concatenating. - * - * If the header does not appear in the message, this method MUST return - * an empty string. - * - * @param string $name Case-insensitive header field name. - * @return string A string of values as provided for the given header - * concatenated together using a comma. If the header does not appear in - * the message, this method MUST return an empty string. - */ - public function getHeaderLine($name) - { - throw new \Exception('Not implemented'); - } - - /** - * Return an instance with the provided value replacing the specified header. - * - * While header names are case-insensitive, the casing of the header will - * be preserved by this function, and returned from getHeaders(). - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * new and/or updated header and value. - * - * @param string $name Case-insensitive header field name. - * @param string|string[] $value Header value(s). - * @return static - * @throws \InvalidArgumentException for invalid header names or values. - */ - public function withHeader($name, $value) - { - throw new \Exception('Not implemented'); - } - - /** - * Return an instance with the specified header appended with the given value. - * - * Existing values for the specified header will be maintained. The new - * value(s) will be appended to the existing list. If the header did not - * exist previously, it will be added. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * new header and/or value. - * - * @param string $name Case-insensitive header field name to add. - * @param string|string[] $value Header value(s). - * @return static - * @throws \InvalidArgumentException for invalid header names or values. - */ - public function withAddedHeader($name, $value) - { - throw new \Exception('Not implemented'); - } - - /** - * Return an instance without the specified header. - * - * Header resolution MUST be done without case-sensitivity. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that removes - * the named header. - * - * @param string $name Case-insensitive header field name to remove. - * @return static - */ - public function withoutHeader($name) - { - throw new \Exception('Not implemented'); - } - - /** - * Gets the body of the message. - * - * @return StreamInterface Returns the body as a stream. - */ - public function getBody() - { - return $this->body; - } - - /** - * Return an instance with the specified message body. - * - * The body MUST be a StreamInterface object. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return a new instance that has the - * new body stream. - * - * @param StreamInterface $body Body. - * @return static - * @throws \InvalidArgumentException When the body is not valid. - */ - public function withBody(StreamInterface $body) - { - throw new \Exception('Not implemented'); - } - - /** - * Retrieves the message's request target. - * - * Retrieves the message's request-target either as it will appear (for - * clients), as it appeared at request (for servers), or as it was - * specified for the instance (see withRequestTarget()). - * - * In most cases, this will be the origin-form of the composed URI, - * unless a value was provided to the concrete implementation (see - * withRequestTarget() below). - * - * If no URI is available, and no request-target has been specifically - * provided, this method MUST return the string "/". - * - * @return string - */ - public function getRequestTarget() - { - throw new \Exception('Not implemented'); - } - - /** - * Return an instance with the specific request-target. - * - * If the request needs a non-origin-form request-target — e.g., for - * specifying an absolute-form, authority-form, or asterisk-form — - * this method may be used to create an instance with the specified - * request-target, verbatim. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * changed request target. - * - * @link http://tools.ietf.org/html/rfc7230#section-5.3 (for the various - * request-target forms allowed in request messages) - * @param mixed $requestTarget - * @return static - */ - public function withRequestTarget($requestTarget) - { - throw new \Exception('Not implemented'); - } - - /** - * Retrieves the HTTP method of the request. - * - * @return string Returns the request method. - */ - public function getMethod() - { - return $this->method; - } - - /** - * Return an instance with the provided HTTP method. - * - * While HTTP method names are typically all uppercase characters, HTTP - * method names are case-sensitive and thus implementations SHOULD NOT - * modify the given string. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * changed request method. - * - * @param string $method Case-sensitive method. - * @return static - * @throws \InvalidArgumentException for invalid HTTP methods. - */ - public function withMethod($method) - { - throw new \Exception('Not implemented'); - } - - /** - * Retrieves the URI instance. - * - * This method MUST return a UriInterface instance. - * - * @link http://tools.ietf.org/html/rfc3986#section-4.3 - * @return UriInterface Returns a UriInterface instance - * representing the URI of the request. - */ - public function getUri() - { - // TODO: Implement getUri() method. - } - - /** - * Returns an instance with the provided URI. - * - * This method MUST update the Host header of the returned request by - * default if the URI contains a host component. If the URI does not - * contain a host component, any pre-existing Host header MUST be carried - * over to the returned request. - * - * You can opt-in to preserving the original state of the Host header by - * setting `$preserveHost` to `true`. When `$preserveHost` is set to - * `true`, this method interacts with the Host header in the following ways: - * - * - If the Host header is missing or empty, and the new URI contains - * a host component, this method MUST update the Host header in the returned - * request. - * - If the Host header is missing or empty, and the new URI does not contain a - * host component, this method MUST NOT update the Host header in the returned - * request. - * - If a Host header is present and non-empty, this method MUST NOT update - * the Host header in the returned request. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * new UriInterface instance. - * - * @link http://tools.ietf.org/html/rfc3986#section-4.3 - * @param UriInterface $uri New request URI to use. - * @param bool $preserveHost Preserve the original state of the Host header. - * @return static - */ - public function withUri(UriInterface $uri, $preserveHost = false) - { - throw new \Exception('Not implemented'); - } - - /** - * Retrieve server parameters. - * - * Retrieves data related to the incoming request environment, - * typically derived from PHP's $_SERVER superglobal. The data IS NOT - * REQUIRED to originate from $_SERVER. - * - * @return array - */ - public function getServerParams() - { - throw new \Exception('Not implemented'); - } - - /** - * Retrieve cookies. - * - * Retrieves cookies sent by the client to the server. - * - * The data MUST be compatible with the structure of the $_COOKIE - * superglobal. - * - * @return array - */ - public function getCookieParams() - { - throw new \Exception('Not implemented'); - } - - /** - * Return an instance with the specified cookies. - * - * The data IS NOT REQUIRED to come from the $_COOKIE superglobal, but MUST - * be compatible with the structure of $_COOKIE. Typically, this data will - * be injected at instantiation. - * - * This method MUST NOT update the related Cookie header of the request - * instance, nor related values in the server params. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * updated cookie values. - * - * @param array $cookies Array of key/value pairs representing cookies. - * @return static - */ - public function withCookieParams(array $cookies) - { - throw new \Exception('Not implemented'); - } - - /** - * Retrieve query string arguments. - * - * Retrieves the deserialized query string arguments, if any. - * - * Note: the query params might not be in sync with the URI or server - * params. If you need to ensure you are only getting the original - * values, you may need to parse the query string from `getUri()->getQuery()` - * or from the `QUERY_STRING` server param. - * - * @return array - */ - public function getQueryParams() - { - return $this->queryParams; - } - - /** - * Return an instance with the specified query string arguments. - * - * These values SHOULD remain immutable over the course of the incoming - * request. They MAY be injected during instantiation, such as from PHP's - * $_GET superglobal, or MAY be derived from some other value such as the - * URI. In cases where the arguments are parsed from the URI, the data - * MUST be compatible with what PHP's parse_str() would return for - * purposes of how duplicate query parameters are handled, and how nested - * sets are handled. - * - * Setting query string arguments MUST NOT change the URI stored by the - * request, nor the values in the server params. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * updated query string arguments. - * - * @param array $query Array of query string arguments, typically from - * $_GET. - * @return static - */ - public function withQueryParams(array $query) - { - throw new \Exception('Not implemented'); - } - - /** - * Retrieve normalized file upload data. - * - * This method returns upload metadata in a normalized tree, with each leaf - * an instance of Psr\Http\Message\UploadedFileInterface. - * - * These values MAY be prepared from $_FILES or the message body during - * instantiation, or MAY be injected via withUploadedFiles(). - * - * @return array An array tree of UploadedFileInterface instances; an empty - * array MUST be returned if no data is present. - */ - public function getUploadedFiles() - { - throw new \Exception('Not implemented'); - } - - /** - * Create a new instance with the specified uploaded files. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * updated body parameters. - * - * @param array $uploadedFiles An array tree of UploadedFileInterface instances. - * @return static - * @throws \InvalidArgumentException if an invalid structure is provided. - */ - public function withUploadedFiles(array $uploadedFiles) - { - throw new \Exception('Not implemented'); - } - - /** - * Retrieve any parameters provided in the request body. - * - * If the request Content-Type is either application/x-www-form-urlencoded - * or multipart/form-data, and the request method is POST, this method MUST - * return the contents of $_POST. - * - * Otherwise, this method may return any results of deserializing - * the request body content; as parsing returns structured content, the - * potential types MUST be arrays or objects only. A null value indicates - * the absence of body content. - * - * @return null|array|object The deserialized body parameters, if any. - * These will typically be an array or object. - */ - public function getParsedBody() - { - return $this->parsedBody; - } - - /** - * Return an instance with the specified body parameters. - * - * These MAY be injected during instantiation. - * - * If the request Content-Type is either application/x-www-form-urlencoded - * or multipart/form-data, and the request method is POST, use this method - * ONLY to inject the contents of $_POST. - * - * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of - * deserializing the request body content. Deserialization/parsing returns - * structured data, and, as such, this method ONLY accepts arrays or objects, - * or a null value if nothing was available to parse. - * - * As an example, if content negotiation determines that the request data - * is a JSON payload, this method could be used to create a request - * instance with the deserialized parameters. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * updated body parameters. - * - * @param null|array|object $data The deserialized body data. This will - * typically be in an array or object. - * @return static - * @throws \InvalidArgumentException if an unsupported argument type is - * provided. - */ - public function withParsedBody($data) - { - throw new \Exception('Not implemented'); - } - - /** - * Retrieve attributes derived from the request. - * - * The request "attributes" may be used to allow injection of any - * parameters derived from the request: e.g., the results of path - * match operations; the results of decrypting cookies; the results of - * deserializing non-form-encoded message bodies; etc. Attributes - * will be application and request specific, and CAN be mutable. - * - * @return array Attributes derived from the request. - */ - public function getAttributes() - { - throw new \Exception('Not implemented'); - } - - /** - * Retrieve a single derived request attribute. - * - * Retrieves a single derived request attribute as described in - * getAttributes(). If the attribute has not been previously set, returns - * the default value as provided. - * - * This method obviates the need for a hasAttribute() method, as it allows - * specifying a default value to return if the attribute is not found. - * - * @see getAttributes() - * @param string $name The attribute name. - * @param mixed $default Default value to return if the attribute does not exist. - * @return mixed - */ - public function getAttribute($name, $default = null) - { - throw new \Exception('Not implemented'); - } - - /** - * Return an instance with the specified derived request attribute. - * - * This method allows setting a single derived request attribute as - * described in getAttributes(). - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * updated attribute. - * - * @see getAttributes() - * @param string $name The attribute name. - * @param mixed $value The value of the attribute. - * @return static - */ - public function withAttribute($name, $value) - { - throw new \Exception('Not implemented'); - } - - /** - * Return an instance that removes the specified derived request attribute. - * - * This method allows removing a single derived request attribute as - * described in getAttributes(). - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that removes - * the attribute. - * - * @see getAttributes() - * @param string $name The attribute name. - * @return static - */ - public function withoutAttribute($name) - { - throw new \Exception('Not implemented'); - } -} diff --git a/tests/Server/Psr7/PsrResponseStub.php b/tests/Server/Psr7/PsrResponseStub.php deleted file mode 100644 index bac2ffb23..000000000 --- a/tests/Server/Psr7/PsrResponseStub.php +++ /dev/null @@ -1,287 +0,0 @@ -getHeaders() as $name => $values) { - * echo $name . ": " . implode(", ", $values); - * } - * - * // Emit headers iteratively: - * foreach ($message->getHeaders() as $name => $values) { - * foreach ($values as $value) { - * header(sprintf('%s: %s', $name, $value), false); - * } - * } - * - * While header names are not case-sensitive, getHeaders() will preserve the - * exact case in which headers were originally specified. - * - * @return string[][] Returns an associative array of the message's headers. Each - * key MUST be a header name, and each value MUST be an array of strings - * for that header. - */ - public function getHeaders() - { - throw new \Exception('Not implemented'); - } - - /** - * Checks if a header exists by the given case-insensitive name. - * - * @param string $name Case-insensitive header field name. - * @return bool Returns true if any header names match the given header - * name using a case-insensitive string comparison. Returns false if - * no matching header name is found in the message. - */ - public function hasHeader($name) - { - throw new \Exception('Not implemented'); - } - - /** - * Retrieves a message header value by the given case-insensitive name. - * - * This method returns an array of all the header values of the given - * case-insensitive header name. - * - * If the header does not appear in the message, this method MUST return an - * empty array. - * - * @param string $name Case-insensitive header field name. - * @return string[] An array of string values as provided for the given - * header. If the header does not appear in the message, this method MUST - * return an empty array. - */ - public function getHeader($name) - { - throw new \Exception('Not implemented'); - } - - /** - * Retrieves a comma-separated string of the values for a single header. - * - * This method returns all of the header values of the given - * case-insensitive header name as a string concatenated together using - * a comma. - * - * NOTE: Not all header values may be appropriately represented using - * comma concatenation. For such headers, use getHeader() instead - * and supply your own delimiter when concatenating. - * - * If the header does not appear in the message, this method MUST return - * an empty string. - * - * @param string $name Case-insensitive header field name. - * @return string A string of values as provided for the given header - * concatenated together using a comma. If the header does not appear in - * the message, this method MUST return an empty string. - */ - public function getHeaderLine($name) - { - throw new \Exception('Not implemented'); - } - - /** - * Return an instance with the provided value replacing the specified header. - * - * While header names are case-insensitive, the casing of the header will - * be preserved by this function, and returned from getHeaders(). - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * new and/or updated header and value. - * - * @param string $name Case-insensitive header field name. - * @param string|string[] $value Header value(s). - * @return static - * @throws \InvalidArgumentException for invalid header names or values. - */ - public function withHeader($name, $value) - { - $tmp = clone $this; - $tmp->headers[$name][] = $value; - - return $tmp; - } - - /** - * Return an instance with the specified header appended with the given value. - * - * Existing values for the specified header will be maintained. The new - * value(s) will be appended to the existing list. If the header did not - * exist previously, it will be added. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * new header and/or value. - * - * @param string $name Case-insensitive header field name to add. - * @param string|string[] $value Header value(s). - * @return static - * @throws \InvalidArgumentException for invalid header names or values. - */ - public function withAddedHeader($name, $value) - { - throw new \Exception('Not implemented'); - } - - /** - * Return an instance without the specified header. - * - * Header resolution MUST be done without case-sensitivity. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that removes - * the named header. - * - * @param string $name Case-insensitive header field name to remove. - * @return static - */ - public function withoutHeader($name) - { - throw new \Exception('Not implemented'); - } - - /** - * Gets the body of the message. - * - * @return StreamInterface Returns the body as a stream. - */ - public function getBody() - { - throw new \Exception('Not implemented'); - } - - /** - * Return an instance with the specified message body. - * - * The body MUST be a StreamInterface object. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return a new instance that has the - * new body stream. - * - * @param StreamInterface $body Body. - * @return static - * @throws \InvalidArgumentException When the body is not valid. - */ - public function withBody(StreamInterface $body) - { - $tmp = clone $this; - $tmp->body = $body; - - return $tmp; - } - - /** - * Gets the response status code. - * - * The status code is a 3-digit integer result code of the server's attempt - * to understand and satisfy the request. - * - * @return int Status code. - */ - public function getStatusCode() - { - throw new \Exception('Not implemented'); - } - - /** - * Return an instance with the specified status code and, optionally, reason phrase. - * - * If no reason phrase is specified, implementations MAY choose to default - * to the RFC 7231 or IANA recommended reason phrase for the response's - * status code. - * - * This method MUST be implemented in such a way as to retain the - * immutability of the message, and MUST return an instance that has the - * updated status and reason phrase. - * - * @link http://tools.ietf.org/html/rfc7231#section-6 - * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml - * @param int $code The 3-digit integer result code to set. - * @param string $reasonPhrase The reason phrase to use with the - * provided status code; if none is provided, implementations MAY - * use the defaults as suggested in the HTTP specification. - * @return static - * @throws \InvalidArgumentException For invalid status code arguments. - */ - public function withStatus($code, $reasonPhrase = '') - { - $tmp = clone $this; - $tmp->statusCode = $code; - - return $tmp; - } - - /** - * Gets the response reason phrase associated with the status code. - * - * Because a reason phrase is not a required element in a response - * status line, the reason phrase value MAY be null. Implementations MAY - * choose to return the default RFC 7231 recommended reason phrase (or those - * listed in the IANA HTTP Status Code Registry) for the response's - * status code. - * - * @link http://tools.ietf.org/html/rfc7231#section-6 - * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml - * @return string Reason phrase; must return an empty string if none present. - */ - public function getReasonPhrase() - { - throw new \Exception('Not implemented'); - } -} diff --git a/tests/Server/Psr7/PsrStreamStub.php b/tests/Server/Psr7/PsrStreamStub.php deleted file mode 100644 index 736ada980..000000000 --- a/tests/Server/Psr7/PsrStreamStub.php +++ /dev/null @@ -1,209 +0,0 @@ -content; - } - - /** - * Closes the stream and any underlying resources. - * - * @return void - */ - public function close() - { - throw new \Exception('Not implemented'); - } - - /** - * Separates any underlying resources from the stream. - * - * After the stream has been detached, the stream is in an unusable state. - * - * @return resource|null Underlying PHP stream, if any - */ - public function detach() - { - throw new \Exception('Not implemented'); - } - - /** - * Get the size of the stream if known. - * - * @return int|null Returns the size in bytes if known, or null if unknown. - */ - public function getSize() - { - return strlen($this->content ?: ''); - } - - /** - * Returns the current position of the file read/write pointer - * - * @return int Position of the file pointer - * @throws \RuntimeException on error. - */ - public function tell() - { - throw new \Exception('Not implemented'); - } - - /** - * Returns true if the stream is at the end of the stream. - * - * @return bool - */ - public function eof() - { - throw new \Exception('Not implemented'); - } - - /** - * Returns whether or not the stream is seekable. - * - * @return bool - */ - public function isSeekable() - { - throw new \Exception('Not implemented'); - } - - /** - * Seek to a position in the stream. - * - * @link http://www.php.net/manual/en/function.fseek.php - * @param int $offset Stream offset - * @param int $whence Specifies how the cursor position will be calculated - * based on the seek offset. Valid values are identical to the built-in - * PHP $whence values for `fseek()`. SEEK_SET: Set position equal to - * offset bytes SEEK_CUR: Set position to current location plus offset - * SEEK_END: Set position to end-of-stream plus offset. - * @throws \RuntimeException on failure. - */ - public function seek($offset, $whence = SEEK_SET) - { - throw new \Exception('Not implemented'); - } - - /** - * Seek to the beginning of the stream. - * - * If the stream is not seekable, this method will raise an exception; - * otherwise, it will perform a seek(0). - * - * @see seek() - * @link http://www.php.net/manual/en/function.fseek.php - * @throws \RuntimeException on failure. - */ - public function rewind() - { - throw new \Exception('Not implemented'); - } - - /** - * Returns whether or not the stream is writable. - * - * @return bool - */ - public function isWritable() - { - return true; - } - - /** - * Write data to the stream. - * - * @param string $string The string that is to be written. - * @return int Returns the number of bytes written to the stream. - * @throws \RuntimeException on failure. - */ - public function write($string) - { - $this->content = $string; - - return strlen($string); - } - - /** - * Returns whether or not the stream is readable. - * - * @return bool - */ - public function isReadable() - { - throw new \Exception('Not implemented'); - } - - /** - * Read data from the stream. - * - * @param int $length Read up to $length bytes from the object and return - * them. Fewer than $length bytes may be returned if underlying stream - * call returns fewer bytes. - * @return string Returns the data read from the stream, or an empty string - * if no bytes are available. - * @throws \RuntimeException if an error occurs. - */ - public function read($length) - { - throw new \Exception('Not implemented'); - } - - /** - * Returns the remaining contents in a string - * - * @return string - * @throws \RuntimeException if unable to read or an error occurs while - * reading. - */ - public function getContents() - { - return $this->content; - } - - /** - * Get stream metadata as an associative array or retrieve a specific key. - * - * The keys returned are identical to the keys returned from PHP's - * stream_get_meta_data() function. - * - * @link http://php.net/manual/en/function.stream-get-meta-data.php - * @param string $key Specific metadata to retrieve. - * @return array|mixed|null Returns an associative array if no key is - * provided. Returns a specific key value if a key is provided and the - * value is found, or null if the key is not found. - */ - public function getMetadata($key = null) - { - throw new \Exception('Not implemented'); - } -} diff --git a/tests/Server/PsrResponseTest.php b/tests/Server/PsrResponseTest.php index 5a2eab525..6edbf3014 100644 --- a/tests/Server/PsrResponseTest.php +++ b/tests/Server/PsrResponseTest.php @@ -6,24 +6,23 @@ use GraphQL\Executor\ExecutionResult; use GraphQL\Server\Helper; -use GraphQL\Tests\Server\Psr7\PsrResponseStub; -use GraphQL\Tests\Server\Psr7\PsrStreamStub; +use Nyholm\Psr7\Response; +use Nyholm\Psr7\Stream; use PHPUnit\Framework\TestCase; use function json_encode; -class PsrResponseTest extends TestCase +final class PsrResponseTest extends TestCase { public function testConvertsResultToPsrResponse() : void { $result = new ExecutionResult(['key' => 'value']); - $stream = new PsrStreamStub(); - $psrResponse = new PsrResponseStub(); + $stream = Stream::create(); + $psrResponse = new Response(); $helper = new Helper(); - /** @var PsrResponseStub $resp */ $resp = $helper->toPsrResponse($result, $psrResponse, $stream); - self::assertSame(json_encode($result), $resp->body->content); - self::assertSame(['Content-Type' => ['application/json']], $resp->headers); + self::assertSame(json_encode($result), (string) $resp->getBody()); + self::assertSame(['Content-Type' => ['application/json']], $resp->getHeaders()); } } diff --git a/tests/Server/QueryExecutionTest.php b/tests/Server/QueryExecutionTest.php index bc16ed622..4e1836e24 100644 --- a/tests/Server/QueryExecutionTest.php +++ b/tests/Server/QueryExecutionTest.php @@ -4,7 +4,7 @@ namespace GraphQL\Tests\Server; -use GraphQL\Error\Debug; +use GraphQL\Error\DebugFlag; use GraphQL\Error\Error; use GraphQL\Error\InvariantViolation; use GraphQL\Executor\ExecutionResult; @@ -14,6 +14,7 @@ use GraphQL\Server\OperationParams; use GraphQL\Server\RequestError; use GraphQL\Server\ServerConfig; +use GraphQL\Tests\PHPUnit\ArraySubsetAsserts; use GraphQL\Validator\DocumentValidator; use GraphQL\Validator\Rules\CustomValidationRule; use GraphQL\Validator\ValidationContext; @@ -23,10 +24,12 @@ class QueryExecutionTest extends ServerTestCase { + use ArraySubsetAsserts; + /** @var ServerConfig */ private $config; - public function setUp() + public function setUp() : void { $schema = $this->buildSchema(); $this->config = ServerConfig::create() @@ -47,7 +50,7 @@ public function testSimpleQueryExecution() : void private function assertQueryResultEquals($expected, $query, $variables = null) { $result = $this->executeQuery($query, $variables); - self::assertArraySubset($expected, $result->toArray(true)); + self::assertArraySubset($expected, $result->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)); return $result; } @@ -69,7 +72,7 @@ public function testReturnsSyntaxErrors() : void $result = $this->executeQuery($query); self::assertNull($result->data); self::assertCount(1, $result->errors); - self::assertContains( + self::assertStringContainsString( 'Syntax Error: Expected Name, found ', $result->errors[0]->getMessage() ); @@ -77,8 +80,8 @@ public function testReturnsSyntaxErrors() : void public function testDebugExceptions() : void { - $debug = Debug::INCLUDE_DEBUG_MESSAGE | Debug::INCLUDE_TRACE; - $this->config->setDebug($debug); + $debugFlag = DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE; + $this->config->setDebugFlag($debugFlag); $query = ' { @@ -107,7 +110,7 @@ public function testDebugExceptions() : void public function testRethrowUnsafeExceptions() : void { - $this->config->setDebug(Debug::RETHROW_UNSAFE_EXCEPTIONS); + $this->config->setDebugFlag(DebugFlag::RETHROW_UNSAFE_EXCEPTIONS); $this->expectException(Unsafe::class); $this->executeQuery(' @@ -171,7 +174,7 @@ public function testPassesCustomValidationRules() : void $called = false; $rules = [ - new CustomValidationRule('SomeRule', static function () use (&$called) { + new CustomValidationRule('SomeRule', static function () use (&$called) : array { $called = true; return []; @@ -191,7 +194,7 @@ public function testAllowsValidationRulesAsClosure() : void $called = false; $params = $doc = $operationType = null; - $this->config->setValidationRules(static function ($p, $d, $o) use (&$called, &$params, &$doc, &$operationType) { + $this->config->setValidationRules(static function ($p, $d, $o) use (&$called, &$params, &$doc, &$operationType) : array { $called = true; $params = $p; $doc = $d; @@ -215,7 +218,7 @@ public function testAllowsDifferentValidationRulesDependingOnOperation() : void $called1 = false; $called2 = false; - $this->config->setValidationRules(static function (OperationParams $params) use ($q1, &$called1, &$called2) { + $this->config->setValidationRules(static function (OperationParams $params) use ($q1, &$called1, &$called2) : array { if ($params->query === $q1) { $called1 = true; @@ -225,7 +228,7 @@ public function testAllowsDifferentValidationRulesDependingOnOperation() : void $called2 = true; return [ - new CustomValidationRule('MyRule', static function (ValidationContext $context) { + new CustomValidationRule('MyRule', static function (ValidationContext $context) : void { $context->reportError(new Error('This is the error we are looking for!')); }), ]; @@ -320,7 +323,7 @@ private function executeBatchedQuery(array $qs) } $helper = new Helper(); $result = $helper->executeBatch($this->config, $batch); - self::assertInternalType('array', $result); + self::assertIsArray($result); self::assertCount(count($qs), $result); foreach ($result as $index => $entry) { @@ -354,7 +357,7 @@ public function testMutationsAreNotAllowedInReadonlyMode() : void public function testAllowsPersistentQueries() : void { $called = false; - $this->config->setPersistentQueryLoader(static function ($queryId, OperationParams $params) use (&$called) { + $this->config->setPersistentQueryLoader(static function ($queryId, OperationParams $params) use (&$called) : string { $called = true; self::assertEquals('some-id', $queryId); @@ -371,7 +374,7 @@ public function testAllowsPersistentQueries() : void // Make sure it allows returning document node: $called = false; - $this->config->setPersistentQueryLoader(static function ($queryId, OperationParams $params) use (&$called) { + $this->config->setPersistentQueryLoader(static function ($queryId, OperationParams $params) use (&$called) : DocumentNode { $called = true; self::assertEquals('some-id', $queryId); @@ -389,7 +392,7 @@ public function testProhibitsInvalidPersistedQueryLoader() : void 'Persistent query loader must return query string or instance of GraphQL\Language\AST\DocumentNode ' . 'but got: {"err":"err"}' ); - $this->config->setPersistentQueryLoader(static function () { + $this->config->setPersistentQueryLoader(static function () : array { return ['err' => 'err']; }); $this->executePersistedQuery('some-id'); @@ -397,7 +400,7 @@ public function testProhibitsInvalidPersistedQueryLoader() : void public function testPersistedQueriesAreStillValidatedByDefault() : void { - $this->config->setPersistentQueryLoader(static function () { + $this->config->setPersistentQueryLoader(static function () : string { return '{invalid}'; }); $result = $this->executePersistedQuery('some-id'); @@ -423,7 +426,7 @@ public function testAllowSkippingValidationForPersistedQueries() : void return '{invalid2}'; }) - ->setValidationRules(static function (OperationParams $params) { + ->setValidationRules(static function (OperationParams $params) : array { if ($params->queryId === 'some-id') { return []; } @@ -454,7 +457,7 @@ public function testProhibitsUnexpectedValidationRules() : void { $this->expectException(InvariantViolation::class); $this->expectExceptionMessage('Expecting validation rules to be array or callable returning array, but got: instance of stdClass'); - $this->config->setValidationRules(static function (OperationParams $params) { + $this->config->setValidationRules(static function (OperationParams $params) : stdClass { return new stdClass(); }); $this->executeQuery('{f1}'); @@ -520,10 +523,10 @@ public function testDeferredsAreSharedAmongAllBatchedQueries() : void ->setQueryBatching(true) ->setRootValue('1') ->setContext([ - 'buffer' => static function ($num) use (&$calls) { + 'buffer' => static function ($num) use (&$calls) : void { $calls[] = sprintf('buffer: %d', $num); }, - 'load' => static function ($num) use (&$calls) { + 'load' => static function ($num) use (&$calls) : string { $calls[] = sprintf('load: %d', $num); return sprintf('loaded: %d', $num); @@ -585,7 +588,7 @@ public function testAllowsContextAsClosure() : void $called = false; $params = $doc = $operationType = null; - $this->config->setContext(static function ($p, $d, $o) use (&$called, &$params, &$doc, &$operationType) { + $this->config->setContext(static function ($p, $d, $o) use (&$called, &$params, &$doc, &$operationType) : void { $called = true; $params = $p; $doc = $d; @@ -605,7 +608,7 @@ public function testAllowsRootValueAsClosure() : void $called = false; $params = $doc = $operationType = null; - $this->config->setRootValue(static function ($p, $d, $o) use (&$called, &$params, &$doc, &$operationType) { + $this->config->setRootValue(static function ($p, $d, $o) use (&$called, &$params, &$doc, &$operationType) : void { $called = true; $params = $p; $doc = $d; @@ -624,7 +627,7 @@ public function testAppliesErrorFormatter() : void { $called = false; $error = null; - $this->config->setErrorFormatter(static function ($e) use (&$called, &$error) { + $this->config->setErrorFormatter(static function ($e) use (&$called, &$error) : array { $called = true; $error = $e; @@ -644,7 +647,7 @@ public function testAppliesErrorFormatter() : void self::assertInstanceOf(Error::class, $error); // Assert debugging still works even with custom formatter - $formatted = $result->toArray(Debug::INCLUDE_TRACE); + $formatted = $result->toArray(DebugFlag::INCLUDE_TRACE); $expected = [ 'errors' => [ [ @@ -661,7 +664,7 @@ public function testAppliesErrorsHandler() : void $called = false; $errors = null; $formatter = null; - $this->config->setErrorsHandler(static function ($e, $f) use (&$called, &$errors, &$formatter) { + $this->config->setErrorsHandler(static function ($e, $f) use (&$called, &$errors, &$formatter) : array { $called = true; $errors = $e; $formatter = $f; @@ -682,9 +685,9 @@ public function testAppliesErrorsHandler() : void ]; self::assertTrue($called); self::assertArraySubset($expected, $formatted); - self::assertInternalType('array', $errors); + self::assertIsArray($errors); self::assertCount(2, $errors); - self::assertInternalType('callable', $formatter); + self::assertIsCallable($formatter); self::assertArraySubset($expected, $formatted); } } diff --git a/tests/Server/RequestParsingTest.php b/tests/Server/RequestParsingTest.php index 9de6eab7d..67cbeda60 100644 --- a/tests/Server/RequestParsingTest.php +++ b/tests/Server/RequestParsingTest.php @@ -8,10 +8,12 @@ use GraphQL\Server\Helper; use GraphQL\Server\OperationParams; use GraphQL\Server\RequestError; -use GraphQL\Tests\Server\Psr7\PsrRequestStub; -use GraphQL\Tests\Server\Psr7\PsrStreamStub; +use InvalidArgumentException; +use Nyholm\Psr7\Request; +use Nyholm\Psr7\Stream; +use Nyholm\Psr7\Uri; use PHPUnit\Framework\TestCase; -use function json_decode; +use function http_build_query; use function json_encode; class RequestParsingTest extends TestCase @@ -43,7 +45,7 @@ private function parseRawRequest($contentType, $content, string $method = 'POST' $helper = new Helper(); - return $helper->parseHttpRequest(static function () use ($content) { + return $helper->parseHttpRequest(static function () use ($content) : string { return $content; }); } @@ -56,22 +58,12 @@ private function parseRawRequest($contentType, $content, string $method = 'POST' */ private function parsePsrRequest($contentType, $content, string $method = 'POST') { - $psrRequestBody = new PsrStreamStub(); - $psrRequestBody->content = $content; - - $psrRequest = new PsrRequestStub(); - $psrRequest->headers['content-type'] = [$contentType]; - $psrRequest->method = $method; - $psrRequest->body = $psrRequestBody; - - if ($contentType === 'application/json') { - $parsedBody = json_decode($content, true); - $parsedBody = $parsedBody === false ? null : $parsedBody; - } else { - $parsedBody = null; - } - - $psrRequest->parsedBody = $parsedBody; + $psrRequest = new Request( + $method, + '', + ['Content-Type' => $contentType], + Stream::create($content) + ); $helper = new Helper(); @@ -106,7 +98,7 @@ private static function assertValidOperationParams( public function testParsesUrlencodedRequest() : void { $query = '{my query}'; - $variables = ['test' => 1, 'test2' => 2]; + $variables = ['test' => '1', 'test2' => '2']; $operation = 'op'; $post = [ @@ -138,7 +130,7 @@ private function parseRawFormUrlencodedRequest($postValue) $helper = new Helper(); - return $helper->parseHttpRequest(static function () { + return $helper->parseHttpRequest(static function () : void { throw new InvariantViolation("Shouldn't read from php://input for urlencoded request"); }); } @@ -150,20 +142,22 @@ private function parseRawFormUrlencodedRequest($postValue) */ private function parsePsrFormUrlEncodedRequest($postValue) { - $psrRequest = new PsrRequestStub(); - $psrRequest->headers['content-type'] = ['application/x-www-form-urlencoded']; - $psrRequest->method = 'POST'; - $psrRequest->parsedBody = $postValue; - $helper = new Helper(); - return $helper->parsePsrRequest($psrRequest); + return $helper->parsePsrRequest( + new Request( + 'POST', + '', + ['Content-Type' => 'application/x-www-form-urlencoded'], + http_build_query($postValue) + ) + ); } public function testParsesGetRequest() : void { $query = '{my query}'; - $variables = ['test' => 1, 'test2' => 2]; + $variables = ['test' => '1', 'test2' => '2']; $operation = 'op'; $get = [ @@ -178,7 +172,7 @@ public function testParsesGetRequest() : void foreach ($parsed as $method => $parsedBody) { self::assertValidOperationParams($parsedBody, $query, null, $variables, $operation, null, $method); - self::assertTrue($parsedBody->isReadonly(), $method); + self::assertTrue($parsedBody->isReadOnly(), $method); } } @@ -194,7 +188,7 @@ private function parseRawGetRequest($getValue) $helper = new Helper(); - return $helper->parseHttpRequest(static function () { + return $helper->parseHttpRequest(static function () : void { throw new InvariantViolation("Shouldn't read from php://input for urlencoded request"); }); } @@ -204,21 +198,19 @@ private function parseRawGetRequest($getValue) * * @return OperationParams[]|OperationParams */ - private function parsePsrGetRequest($getValue) + private function parsePsrGetRequest(array $getValue) { - $psrRequest = new PsrRequestStub(); - $psrRequest->method = 'GET'; - $psrRequest->queryParams = $getValue; - $helper = new Helper(); - return $helper->parsePsrRequest($psrRequest); + return $helper->parsePsrRequest( + new Request('GET', (new Uri())->withQuery(http_build_query($getValue))) + ); } public function testParsesMultipartFormdataRequest() : void { $query = '{my query}'; - $variables = ['test' => 1, 'test2' => 2]; + $variables = ['test' => '1', 'test2' => '2']; $operation = 'op'; $post = [ @@ -250,7 +242,7 @@ private function parseRawMultipartFormDataRequest($postValue) $helper = new Helper(); - return $helper->parseHttpRequest(static function () { + return $helper->parseHttpRequest(static function () : void { throw new InvariantViolation("Shouldn't read from php://input for multipart/form-data request"); }); } @@ -262,14 +254,16 @@ private function parseRawMultipartFormDataRequest($postValue) */ private function parsePsrMultipartFormDataRequest($postValue) { - $psrRequest = new PsrRequestStub(); - $psrRequest->headers['content-type'] = ['multipart/form-data; boundary=----FormBoundary']; - $psrRequest->method = 'POST'; - $psrRequest->parsedBody = $postValue; - $helper = new Helper(); - return $helper->parsePsrRequest($psrRequest); + return $helper->parsePsrRequest( + new Request( + 'POST', + '', + ['Content-Type' => 'multipart/form-data; boundary=----FormBoundary'], + http_build_query($postValue) + ) + ); } public function testParsesJSONRequest() : void @@ -378,7 +372,7 @@ public function testParsesBatchJSONRequest() : void 'psr' => $this->parsePsrRequest('application/json', json_encode($body)), ]; foreach ($parsed as $method => $parsedBody) { - self::assertInternalType('array', $parsedBody, $method); + self::assertIsArray($parsedBody, $method); self::assertCount(2, $parsedBody, $method); self::assertValidOperationParams( $parsedBody[0], @@ -415,7 +409,7 @@ public function testFailsParsingInvalidRawJsonRequestPsr() : void $body = 'not really{} a json'; $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage('PSR-7 request is expected to provide parsed body for "application/json" requests but got null'); + $this->expectExceptionMessage('Expected to receive a JSON array in body for "application/json" PSR-7 request'); $this->parsePsrRequest('application/json', $body); } @@ -426,8 +420,8 @@ public function testFailsParsingNonPreParsedPsrRequest() : void self::fail('Expected exception not thrown'); } catch (InvariantViolation $e) { // Expecting parsing exception to be thrown somewhere else: - self::assertEquals( - 'PSR-7 request is expected to provide parsed body for "application/json" requests but got null', + self::assertSame( + 'Expected to receive a JSON array in body for "application/json" PSR-7 request', $e->getMessage() ); } @@ -483,8 +477,7 @@ public function testFailsWithMissingContentTypeRaw() : void public function testFailsWithMissingContentTypePsr() : void { - $this->expectException(RequestError::class); - $this->expectExceptionMessage('Missing "Content-Type" header'); + $this->expectException(InvalidArgumentException::class); $this->parsePsrRequest(null, 'test'); } diff --git a/tests/Server/RequestValidationTest.php b/tests/Server/RequestValidationTest.php index c4184074f..57878d6d2 100644 --- a/tests/Server/RequestValidationTest.php +++ b/tests/Server/RequestValidationTest.php @@ -64,7 +64,7 @@ private function assertInputError($parsedRequest, $expectedMessage) { $helper = new Helper(); $errors = $helper->validateOperationParams($parsedRequest); - if (! empty($errors[0])) { + if (isset($errors[0])) { self::assertEquals($expectedMessage, $errors[0]->getMessage()); } else { self::fail('Expected error not returned'); diff --git a/tests/Server/ServerConfigTest.php b/tests/Server/ServerConfigTest.php index a596ab300..b9cae9baf 100644 --- a/tests/Server/ServerConfigTest.php +++ b/tests/Server/ServerConfigTest.php @@ -4,6 +4,7 @@ namespace GraphQL\Tests\Server; +use GraphQL\Error\DebugFlag; use GraphQL\Error\InvariantViolation; use GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter; use GraphQL\Server\ServerConfig; @@ -27,7 +28,7 @@ public function testDefaults() : void self::assertNull($config->getValidationRules()); self::assertNull($config->getFieldResolver()); self::assertNull($config->getPersistentQueryLoader()); - self::assertFalse($config->getDebug()); + self::assertSame(DebugFlag::NONE, $config->getDebugFlag()); self::assertFalse($config->getQueryBatching()); } @@ -74,7 +75,7 @@ public function testAllowsSettingErrorFormatter() : void { $config = ServerConfig::create(); - $formatter = static function () { + $formatter = static function () : void { }; $config->setErrorFormatter($formatter); self::assertSame($formatter, $config->getErrorFormatter()); @@ -88,7 +89,7 @@ public function testAllowsSettingErrorsHandler() : void { $config = ServerConfig::create(); - $handler = static function () { + $handler = static function () : void { }; $config->setErrorsHandler($handler); self::assertSame($handler, $config->getErrorsHandler()); @@ -119,14 +120,14 @@ public function testAllowsSettingValidationRules() : void $config->setValidationRules($rules); self::assertSame($rules, $config->getValidationRules()); - $rules = [static function () { + $rules = [static function () : void { }, ]; $config->setValidationRules($rules); self::assertSame($rules, $config->getValidationRules()); - $rules = static function () { - return [static function () { + $rules = static function () : array { + return [static function () : void { }, ]; }; @@ -138,7 +139,7 @@ public function testAllowsSettingDefaultFieldResolver() : void { $config = ServerConfig::create(); - $resolver = static function () { + $resolver = static function () : void { }; $config->setFieldResolver($resolver); self::assertSame($resolver, $config->getFieldResolver()); @@ -152,7 +153,7 @@ public function testAllowsSettingPersistedQueryLoader() : void { $config = ServerConfig::create(); - $loader = static function () { + $loader = static function () : void { }; $config->setPersistentQueryLoader($loader); self::assertSame($loader, $config->getPersistentQueryLoader()); @@ -166,11 +167,11 @@ public function testAllowsSettingCatchPhpErrors() : void { $config = ServerConfig::create(); - $config->setDebug(true); - self::assertTrue($config->getDebug()); + $config->setDebugFlag(DebugFlag::INCLUDE_DEBUG_MESSAGE); + self::assertEquals(DebugFlag::INCLUDE_DEBUG_MESSAGE, $config->getDebugFlag()); - $config->setDebug(false); - self::assertFalse($config->getDebug()); + $config->setDebugFlag(DebugFlag::NONE); + self::assertEquals(DebugFlag::NONE, $config->getDebugFlag()); } public function testAcceptsArray() : void @@ -181,17 +182,17 @@ public function testAcceptsArray() : void ]), 'context' => new stdClass(), 'rootValue' => new stdClass(), - 'errorFormatter' => static function () { + 'errorFormatter' => static function () : void { }, 'promiseAdapter' => new SyncPromiseAdapter(), - 'validationRules' => [static function () { + 'validationRules' => [static function () : void { }, ], - 'fieldResolver' => static function () { + 'fieldResolver' => static function () : void { }, - 'persistentQueryLoader' => static function () { + 'persistentQueryLoader' => static function () : void { }, - 'debug' => true, + 'debugFlag' => DebugFlag::INCLUDE_DEBUG_MESSAGE, 'queryBatching' => true, ]; @@ -205,7 +206,7 @@ public function testAcceptsArray() : void self::assertSame($arr['validationRules'], $config->getValidationRules()); self::assertSame($arr['fieldResolver'], $config->getFieldResolver()); self::assertSame($arr['persistentQueryLoader'], $config->getPersistentQueryLoader()); - self::assertTrue($config->getDebug()); + self::assertSame(DebugFlag::INCLUDE_DEBUG_MESSAGE, $config->getDebugFlag()); self::assertTrue($config->getQueryBatching()); } diff --git a/tests/Server/ServerTestCase.php b/tests/Server/ServerTestCase.php index 072acba46..45db91fde 100644 --- a/tests/Server/ServerTestCase.php +++ b/tests/Server/ServerTestCase.php @@ -25,16 +25,20 @@ protected function buildSchema() 'fields' => [ 'f1' => [ 'type' => Type::string(), - 'resolve' => static function ($root, $args, $context, $info) { + 'resolve' => static function ($rootValue, $args, $context, $info) { return $info->fieldName; }, ], 'fieldWithPhpError' => [ 'type' => Type::string(), - 'resolve' => static function ($root, $args, $context, $info) { + 'resolve' => static function ($rootValue, $args, $context, $info) { trigger_error('deprecated', E_USER_DEPRECATED); trigger_error('notice', E_USER_NOTICE); trigger_error('warning', E_USER_WARNING); + + /** + * @var array + */ $a = []; $a['test']; // should produce PHP notice @@ -43,20 +47,20 @@ protected function buildSchema() ], 'fieldWithSafeException' => [ 'type' => Type::string(), - 'resolve' => static function () { + 'resolve' => static function () : void { throw new UserError('This is the exception we want'); }, ], 'fieldWithUnsafeException' => [ 'type' => Type::string(), - 'resolve' => static function () { + 'resolve' => static function () : void { throw new Unsafe('This exception should not be shown to the user'); }, ], 'testContextAndRootValue' => [ 'type' => Type::string(), - 'resolve' => static function ($root, $args, $context, $info) { - $context->testedRootValue = $root; + 'resolve' => static function ($rootValue, $args, $context, $info) { + $context->testedRootValue = $rootValue; return $info->fieldName; }, @@ -68,7 +72,7 @@ protected function buildSchema() 'type' => Type::nonNull(Type::string()), ], ], - 'resolve' => static function ($root, $args) { + 'resolve' => static function ($rootValue, $args) { return $args['arg']; }, ], @@ -79,7 +83,7 @@ protected function buildSchema() 'type' => Type::nonNull(Type::int()), ], ], - 'resolve' => static function ($root, $args, $context) { + 'resolve' => static function ($rootValue, $args, $context) { $context['buffer']($args['num']); return new Deferred(static function () use ($args, $context) { diff --git a/tests/Server/StandardServerTest.php b/tests/Server/StandardServerTest.php index a672141c3..405dbe682 100644 --- a/tests/Server/StandardServerTest.php +++ b/tests/Server/StandardServerTest.php @@ -4,19 +4,25 @@ namespace GraphQL\Tests\Server; +use GraphQL\Error\DebugFlag; use GraphQL\Executor\ExecutionResult; use GraphQL\Server\Helper; use GraphQL\Server\ServerConfig; use GraphQL\Server\StandardServer; -use GraphQL\Tests\Server\Psr7\PsrRequestStub; +use GraphQL\Tests\PHPUnit\ArraySubsetAsserts; +use Nyholm\Psr7\Request; +use Nyholm\Psr7\Stream; +use Psr\Http\Message\RequestInterface; use function json_encode; class StandardServerTest extends ServerTestCase { + use ArraySubsetAsserts; + /** @var ServerConfig */ private $config; - public function setUp() + public function setUp() : void { $schema = $this->buildSchema(); $this->config = ServerConfig::create() @@ -35,7 +41,7 @@ public function testSimpleRequestExecutionWithOutsideParsing() : void 'data' => ['f1' => 'f1'], ]; - self::assertEquals($expected, $result->toArray(true)); + self::assertEquals($expected, $result->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)); } private function parseRawRequest($contentType, $content, $method = 'POST') @@ -58,24 +64,24 @@ public function testSimplePsrRequestExecution() : void 'data' => ['f1' => 'f1'], ]; - $request = $this->preparePsrRequest('application/json', $body); + $request = $this->preparePsrRequest('application/json', json_encode($body)); $this->assertPsrRequestEquals($expected, $request); } - private function preparePsrRequest($contentType, $parsedBody) + private function preparePsrRequest($contentType, $body) : RequestInterface { - $psrRequest = new PsrRequestStub(); - $psrRequest->headers['content-type'] = [$contentType]; - $psrRequest->method = 'POST'; - $psrRequest->parsedBody = $parsedBody; - - return $psrRequest; + return new Request( + 'POST', + '', + ['Content-Type' => $contentType], + $body + ); } private function assertPsrRequestEquals($expected, $request) { $result = $this->executePsrRequest($request); - self::assertArraySubset($expected, $result->toArray(true)); + self::assertArraySubset($expected, $result->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)); return $result; } @@ -100,7 +106,7 @@ public function testMultipleOperationPsrRequestExecution() : void 'data' => ['f1' => 'f1'], ]; - $request = $this->preparePsrRequest('application/json', $body); + $request = $this->preparePsrRequest('application/json', json_encode($body)); $this->assertPsrRequestEquals($expected, $request); } } diff --git a/tests/StarWarsIntrospectionTest.php b/tests/StarWarsIntrospectionTest.php index 35da7daca..94c2b7e70 100644 --- a/tests/StarWarsIntrospectionTest.php +++ b/tests/StarWarsIntrospectionTest.php @@ -11,6 +11,7 @@ class StarWarsIntrospectionTest extends TestCase { // Star Wars Introspection Tests // Basic Introspection + /** * @see it('Allows querying the schema for types') */ diff --git a/tests/StarWarsSchema.php b/tests/StarWarsSchema.php index b9240aff1..4304914c8 100644 --- a/tests/StarWarsSchema.php +++ b/tests/StarWarsSchema.php @@ -109,7 +109,7 @@ public static function build() : Schema $characterInterface = new InterfaceType([ 'name' => 'Character', 'description' => 'A character in the Star Wars Trilogy', - 'fields' => static function () use (&$characterInterface, $episodeEnum) { + 'fields' => static function () use (&$characterInterface, $episodeEnum) : array { return [ 'id' => [ 'type' => Type::nonNull(Type::string()), @@ -165,12 +165,12 @@ public static function build() : Schema 'friends' => [ 'type' => Type::listOf($characterInterface), 'description' => 'The friends of the human, or an empty list if they have none.', - 'resolve' => static function ($human, $args, $context, ResolveInfo $info) { + 'resolve' => static function ($human, $args, $context, ResolveInfo $info) : array { $fieldSelection = $info->getFieldSelection(); $fieldSelection['id'] = true; return array_map( - static function ($friend) use ($fieldSelection) { + static function ($friend) use ($fieldSelection) : array { return array_intersect_key($friend, $fieldSelection); }, StarWarsData::getFriends($human) @@ -188,7 +188,7 @@ static function ($friend) use ($fieldSelection) { 'secretBackstory' => [ 'type' => Type::string(), 'description' => 'Where are they from and how they came to be who they are.', - 'resolve' => static function () { + 'resolve' => static function () : void { // This is to demonstrate error reporting throw new Exception('secretBackstory is secret.'); }, @@ -236,7 +236,7 @@ static function ($friend) use ($fieldSelection) { 'secretBackstory' => [ 'type' => Type::string(), 'description' => 'Construction date and the name of the designer.', - 'resolve' => static function () { + 'resolve' => static function () : void { // This is to demonstrate error reporting throw new Exception('secretBackstory is secret.'); }, @@ -273,7 +273,7 @@ static function ($friend) use ($fieldSelection) { 'type' => $episodeEnum, ], ], - 'resolve' => static function ($root, $args) { + 'resolve' => static function ($rootValue, $args) : array { return StarWarsData::getHero($args['episode'] ?? null); }, ], @@ -286,7 +286,7 @@ static function ($friend) use ($fieldSelection) { 'type' => Type::nonNull(Type::string()), ], ], - 'resolve' => static function ($root, $args) { + 'resolve' => static function ($rootValue, $args) { $humans = StarWarsData::humans(); return $humans[$args['id']] ?? null; @@ -301,7 +301,7 @@ static function ($friend) use ($fieldSelection) { 'type' => Type::nonNull(Type::string()), ], ], - 'resolve' => static function ($root, $args) { + 'resolve' => static function ($rootValue, $args) { $droids = StarWarsData::droids(); return $droids[$args['id']] ?? null; diff --git a/tests/StarWarsValidationTest.php b/tests/StarWarsValidationTest.php index cdee2e317..f444718e9 100644 --- a/tests/StarWarsValidationTest.php +++ b/tests/StarWarsValidationTest.php @@ -12,6 +12,7 @@ class StarWarsValidationTest extends TestCase { // Star Wars Validation Tests // Basic Queries + /** * @see it('Validates a complex but valid query') */ @@ -36,7 +37,7 @@ public function testValidatesAComplexButValidQuery() : void } '; $errors = $this->validationErrors($query); - self::assertEquals(true, empty($errors)); + self::assertCount(0, $errors); } /** @@ -62,7 +63,7 @@ public function testThatNonExistentFieldsAreInvalid() : void } '; $errors = $this->validationErrors($query); - self::assertEquals(false, empty($errors)); + self::assertCount(1, $errors); } /** @@ -77,7 +78,7 @@ public function testRequiresFieldsOnObjects() : void '; $errors = $this->validationErrors($query); - self::assertEquals(false, empty($errors)); + self::assertCount(1, $errors); } /** @@ -95,7 +96,7 @@ public function testDisallowsFieldsOnScalars() : void } '; $errors = $this->validationErrors($query); - self::assertEquals(false, empty($errors)); + self::assertCount(1, $errors); } /** @@ -112,7 +113,7 @@ public function testDisallowsObjectFieldsOnInterfaces() : void } '; $errors = $this->validationErrors($query); - self::assertEquals(false, empty($errors)); + self::assertCount(1, $errors); } /** @@ -133,7 +134,7 @@ public function testAllowsObjectFieldsInFragments() : void } '; $errors = $this->validationErrors($query); - self::assertEquals(true, empty($errors)); + self::assertCount(0, $errors); } /** @@ -152,6 +153,6 @@ public function testAllowsObjectFieldsInInlineFragments() : void } '; $errors = $this->validationErrors($query); - self::assertEquals(true, empty($errors)); + self::assertCount(0, $errors); } } diff --git a/tests/Type/DefinitionTest.php b/tests/Type/DefinitionTest.php index 5bfcb829d..4a06b6450 100644 --- a/tests/Type/DefinitionTest.php +++ b/tests/Type/DefinitionTest.php @@ -5,10 +5,14 @@ namespace GraphQL\Tests\Type; use GraphQL\Error\InvariantViolation; +use GraphQL\Error\Warning; +use GraphQL\Tests\PHPUnit\ArraySubsetAsserts; use GraphQL\Tests\Type\TestClasses\MyCustomType; use GraphQL\Tests\Type\TestClasses\OtherCustom; use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\EnumType; +use GraphQL\Type\Definition\FieldDefinition; +use GraphQL\Type\Definition\InputObjectField; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ListOfType; @@ -17,17 +21,17 @@ use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Schema; -use GraphQL\Utils\Utils; +use PHPUnit\Framework\Error\Warning as PhpUnitWarning; use PHPUnit\Framework\TestCase; use stdClass; -use Throwable; use function count; -use function get_class; use function json_encode; use function sprintf; class DefinitionTest extends TestCase { + use ArraySubsetAsserts; + /** @var ObjectType */ public $blogImage; @@ -67,7 +71,7 @@ class DefinitionTest extends TestCase /** @var CustomScalarType */ public $scalarType; - public function setUp() + public function setUp() : void { $this->objectType = new ObjectType(['name' => 'Object', 'fields' => ['tmp' => Type::string()]]); $this->interfaceType = new InterfaceType(['name' => 'Interface']); @@ -82,11 +86,11 @@ public function setUp() $this->scalarType = new CustomScalarType([ 'name' => 'Scalar', - 'serialize' => static function () { + 'serialize' => static function () : void { }, - 'parseValue' => static function () { + 'parseValue' => static function () : void { }, - 'parseLiteral' => static function () { + 'parseLiteral' => static function () : void { }, ]); @@ -101,7 +105,7 @@ public function setUp() $this->blogAuthor = new ObjectType([ 'name' => 'Author', - 'fields' => function () { + 'fields' => function () : array { return [ 'id' => ['type' => Type::string()], 'name' => ['type' => Type::string()], @@ -205,6 +209,106 @@ public function testDefinesAQueryOnlySchema() : void self::assertSame($this->blogArticle, $feedFieldType->getWrappedType()); } + public function testFieldDefinitionPublicTypeGetDeprecation() : void + { + $fieldDef = FieldDefinition::create([ + 'type' => Type::string(), + 'name' => 'GenericField', + ]); + + Warning::setWarningHandler(static function ($message) : void { + self::assertEquals($message, 'The public getter for \'type\' on FieldDefinition has been deprecated and will be removed in the next major version. Please update your code to use the \'getType\' method.'); + }); + + self::assertFalse(isset($fieldDef->nonExistentProp)); + $fieldDef->nonExistentProp = 'someValue'; + self::assertTrue(isset($fieldDef->nonExistentProp)); + + // @phpstan-ignore-next-line type is private, but we're allowing its access temporarily via a magic method + $type = $fieldDef->type; + } + + public function testFieldDefinitionPublicTypeSetDeprecation() : void + { + $fieldDef = FieldDefinition::create([ + 'type' => Type::string(), + 'name' => 'GenericField', + ]); + + Warning::setWarningHandler(static function ($message) : void { + self::assertEquals($message, 'The public setter for \'type\' on FieldDefinition has been deprecated and will be removed in the next major version.'); + }); + + // @phpstan-ignore-next-line type is private, but we're allowing its access temporarily via a magic method + $fieldDef->type = Type::int(); + + $fieldDef->nonExistentProp = 'someValue'; + self::assertEquals($fieldDef->nonExistentProp, 'someValue'); + } + + public function testFieldDefinitionPublicTypeIssetDeprecation() : void + { + $fieldDef = FieldDefinition::create([ + 'type' => Type::string(), + 'name' => 'GenericField', + ]); + + Warning::setWarningHandler(static function ($message) : void { + self::assertEquals($message, 'The public getter for \'type\' on FieldDefinition has been deprecated and will be removed in the next major version. Please update your code to use the \'getType\' method.'); + }); + + isset($fieldDef->type); + } + + public function testInputObjectFieldPublicTypeGetDeprecation() : void + { + $fieldDef = new InputObjectField([ + 'type' => Type::string(), + 'name' => 'GenericField', + ]); + + Warning::setWarningHandler(static function ($message) : void { + self::assertEquals($message, 'The public getter for \'type\' on InputObjectField has been deprecated and will be removed in the next major version. Please update your code to use the \'getType\' method.'); + }); + + // @phpstan-ignore-next-line type is private, but we're allowing its access temporarily via a magic method + $type = $fieldDef->type; + } + + public function testInputObjectFieldPublicTypeSetDeprecation() : void + { + $fieldDef = new InputObjectField([ + 'type' => Type::string(), + 'name' => 'GenericField', + ]); + + Warning::setWarningHandler(static function ($message) : void { + self::assertEquals($message, 'The public setter for \'type\' on InputObjectField has been deprecated and will be removed in the next major version.'); + }); + + // @phpstan-ignore-next-line type is private, but we're allowing its access temporarily via a magic method + $fieldDef->type = Type::int(); + } + + public function testInputObjectFieldPublicTypeIssetDeprecation() : void + { + $fieldDef = new InputObjectField([ + 'type' => Type::string(), + 'name' => 'GenericField', + ]); + + Warning::setWarningHandler(static function ($message) : void { + self::assertEquals($message, 'The public getter for \'type\' on InputObjectField has been deprecated and will be removed in the next major version. Please update your code to use the \'getType\' method.'); + }); + + isset($fieldDef->type); + + self::assertFalse(isset($fieldDef->nonExistentProp)); + $fieldDef->nonExistentProp = 'someValue'; + self::assertTrue(isset($fieldDef->nonExistentProp)); + self::assertEquals($fieldDef->nonExistentProp, 'someValue'); + } + /** * @see it('defines a mutation schema') */ @@ -405,7 +509,7 @@ public function testIncludesInterfacesThunkSubtypesInTheTypeMap() : void 'fields' => [ 'f' => ['type' => Type::int()], ], - 'interfaces' => static function () use (&$someInterface) { + 'interfaces' => static function () use (&$someInterface) : array { return [$someInterface]; }, ]); @@ -481,6 +585,16 @@ public function testIdentifiesInputTypes() : void [$this->unionType, false], [$this->enumType, true], [$this->inputObjectType, true], + + [Type::boolean(), true], + [Type::float(),true ], + [Type::id(), true], + [Type::int(), true], + [Type::listOf(Type::string()), true], + [Type::listOf($this->objectType), false], + [Type::nonNull(Type::string()), true], + [Type::nonNull($this->objectType), false], + [Type::string(), true], ]; foreach ($expected as $index => $entry) { @@ -504,6 +618,16 @@ public function testIdentifiesOutputTypes() : void [$this->unionType, true], [$this->enumType, true], [$this->inputObjectType, false], + + [Type::boolean(), true], + [Type::float(),true ], + [Type::id(), true], + [Type::int(), true], + [Type::listOf(Type::string()), true], + [Type::listOf($this->objectType), true], + [Type::nonNull(Type::string()), true], + [Type::nonNull($this->objectType), true], + [Type::string(), true], ]; foreach ($expected as $index => $entry) { @@ -515,18 +639,6 @@ public function testIdentifiesOutputTypes() : void } } - /** - * @see it('prohibits nesting NonNull inside NonNull') - */ - public function testProhibitsNestingNonNullInsideNonNull() : void - { - $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage( - 'Expected Int! to be a GraphQL nullable type.' - ); - Type::nonNull(Type::nonNull(Type::int())); - } - /** * @see it('allows a thunk for Union member types') */ @@ -534,7 +646,7 @@ public function testAllowsThunkForUnionTypes() : void { $union = new UnionType([ 'name' => 'ThunkUnion', - 'types' => function () { + 'types' => function () : array { return [$this->objectType]; }, ]); @@ -559,7 +671,7 @@ public function testAllowsRecursiveDefinitions() : void $user = new ObjectType([ 'name' => 'User', - 'fields' => static function () use (&$blog, &$called) { + 'fields' => static function () use (&$blog, &$called) : array { self::assertNotNull($blog, 'Blog type is expected to be defined at this point, but it is null'); $called = true; @@ -568,20 +680,20 @@ public function testAllowsRecursiveDefinitions() : void 'blogs' => ['type' => Type::nonNull(Type::listOf(Type::nonNull($blog)))], ]; }, - 'interfaces' => static function () use ($node) { + 'interfaces' => static function () use ($node) : array { return [$node]; }, ]); $blog = new ObjectType([ 'name' => 'Blog', - 'fields' => static function () use ($user) { + 'fields' => static function () use ($user) : array { return [ 'id' => ['type' => Type::nonNull(Type::id())], 'owner' => ['type' => Type::nonNull($user)], ]; }, - 'interfaces' => static function () use ($node) { + 'interfaces' => static function () use ($node) : array { return [$node]; }, ]); @@ -603,10 +715,14 @@ public function testAllowsRecursiveDefinitions() : void self::assertEquals([$node], $user->getInterfaces()); self::assertNotNull($user->getField('blogs')); - self::assertSame($blog, $user->getField('blogs')->getType()->getWrappedType(true)); + /** @var NonNull $blogFieldReturnType */ + $blogFieldReturnType = $user->getField('blogs')->getType(); + self::assertSame($blog, $blogFieldReturnType->getWrappedType(true)); self::assertNotNull($blog->getField('owner')); - self::assertSame($user, $blog->getField('owner')->getType()->getWrappedType(true)); + /** @var NonNull $ownerFieldReturnType */ + $ownerFieldReturnType = $blog->getField('owner')->getType(); + self::assertSame($user, $ownerFieldReturnType->getWrappedType(true)); } public function testInputObjectTypeAllowsRecursiveDefinitions() : void @@ -614,7 +730,7 @@ public function testInputObjectTypeAllowsRecursiveDefinitions() : void $called = false; $inputObject = new InputObjectType([ 'name' => 'InputObject', - 'fields' => static function () use (&$inputObject, &$called) { + 'fields' => static function () use (&$inputObject, &$called) : array { $called = true; return [ @@ -651,7 +767,7 @@ public function testInterfaceTypeAllowsRecursiveDefinitions() : void $called = false; $interface = new InterfaceType([ 'name' => 'SomeInterface', - 'fields' => static function () use (&$interface, &$called) { + 'fields' => static function () use (&$interface, &$called) : array { $called = true; return [ @@ -681,7 +797,7 @@ public function testAllowsShorthandFieldDefinition() : void { $interface = new InterfaceType([ 'name' => 'SomeInterface', - 'fields' => static function () use (&$interface) { + 'fields' => static function () use (&$interface) : array { return [ 'value' => Type::string(), 'nested' => $interface, @@ -702,19 +818,24 @@ public function testAllowsShorthandFieldDefinition() : void $schema = new Schema(['query' => $query]); - $valueField = $schema->getType('SomeInterface')->getField('value'); - $nestedField = $schema->getType('SomeInterface')->getField('nested'); + /** @var InterfaceType $SomeInterface */ + $SomeInterface = $schema->getType('SomeInterface'); + $valueField = $SomeInterface->getField('value'); self::assertEquals(Type::string(), $valueField->getType()); + + $nestedField = $SomeInterface->getField('nested'); self::assertEquals($interface, $nestedField->getType()); - $withArg = $schema->getType('SomeInterface')->getField('withArg'); + $withArg = $SomeInterface->getField('withArg'); self::assertEquals(Type::string(), $withArg->getType()); self::assertEquals('arg1', $withArg->args[0]->name); self::assertEquals(Type::int(), $withArg->args[0]->getType()); - $testField = $schema->getType('Query')->getField('test'); + /** @var ObjectType $Query */ + $Query = $schema->getType('Query'); + $testField = $Query->getField('test'); self::assertEquals($interface, $testField->getType()); self::assertEquals('test', $testField->name); } @@ -732,11 +853,11 @@ public function testAllowsOverridingInternalTypes() : void { $idType = new CustomScalarType([ 'name' => 'ID', - 'serialize' => static function () { + 'serialize' => static function () : void { }, - 'parseValue' => static function () { + 'parseValue' => static function () : void { }, - 'parseLiteral' => static function () { + 'parseLiteral' => static function () : void { }, ]); @@ -757,13 +878,13 @@ public function testAcceptsAnObjectTypeWithAFieldFunction() : void { $objType = new ObjectType([ 'name' => 'SomeObject', - 'fields' => static function () { + 'fields' => static function () : array { return [ 'f' => ['type' => Type::string()], ]; }, ]); - $objType->assertValid(true); + $objType->assertValid(); self::assertSame(Type::string(), $objType->getField('f')->getType()); } @@ -807,7 +928,7 @@ public function testRejectsAnObjectTypeWithAFieldFunctionThatReturnsIncorrectTyp { $objType = new ObjectType([ 'name' => 'SomeObject', - 'fields' => static function () { + 'fields' => static function () : array { return [['field' => Type::string()]]; }, ]); @@ -888,7 +1009,7 @@ public function testAcceptsAnObjectTypeWithInterfacesAsAFunctionReturningAnArray { $objType = new ObjectType([ 'name' => 'SomeObject', - 'interfaces' => function () { + 'interfaces' => function () : array { return [$this->interfaceType]; }, 'fields' => ['f' => ['type' => Type::string()]], @@ -920,7 +1041,7 @@ public function testRejectsAnObjectTypeWithInterfacesAsAFunctionReturningAnIncor { $objType = new ObjectType([ 'name' => 'SomeObject', - 'interfaces' => static function () { + 'interfaces' => static function () : stdClass { return new stdClass(); }, 'fields' => ['f' => ['type' => Type::string()]], @@ -941,7 +1062,7 @@ public function testAcceptsALambdaAsAnObjectFieldResolver() : void { $this->expectNotToPerformAssertions(); // should not throw: - $this->schemaWithObjectWithFieldResolver(static function () { + $this->schemaWithObjectWithFieldResolver(static function () : void { }); } @@ -1017,6 +1138,70 @@ public function testAcceptsAnInterfaceTypeDefiningResolveType() : void ); } + /** + * @see it('accepts an Interface type with an array of interfaces') + */ + public function testAcceptsAnInterfaceTypeWithAnArrayOfInterfaces() : void + { + $interfaceType = new InterfaceType([ + 'name' => 'AnotherInterface', + 'fields' => [], + 'interfaces' => [$this->interfaceType], + ]); + self::assertSame($this->interfaceType, $interfaceType->getInterfaces()[0]); + } + + /** + * @see it('accepts an Interface type with interfaces as a function returning an array') + */ + public function testAcceptsAnInterfaceTypeWithInterfacesAsAFunctionReturningAnArray() : void + { + $interfaceType = new InterfaceType([ + 'name' => 'AnotherInterface', + 'fields' => [], + 'interfaces' => function () : array { + return [$this->interfaceType]; + }, + ]); + self::assertSame($this->interfaceType, $interfaceType->getInterfaces()[0]); + } + + /** + * @see it('rejects an Interface type with incorrectly typed interfaces') + */ + public function testRejectsAnInterfaceTypeWithIncorrectlyTypedInterfaces() : void + { + $objType = new InterfaceType([ + 'name' => 'AnotherInterface', + 'interfaces' => new stdClass(), + 'fields' => [], + ]); + $this->expectException(InvariantViolation::class); + $this->expectExceptionMessage( + 'AnotherInterface interfaces must be an Array or a callable which returns an Array.' + ); + $objType->getInterfaces(); + } + + /** + * @see it('rejects an Interface type with interfaces as a function returning an incorrect type') + */ + public function testRejectsAnInterfaceTypeWithInterfacesAsAFunctionReturningAnIncorrectType() : void + { + $objType = new ObjectType([ + 'name' => 'AnotherInterface', + 'interfaces' => static function () : stdClass { + return new stdClass(); + }, + 'fields' => [], + ]); + $this->expectException(InvariantViolation::class); + $this->expectExceptionMessage( + 'AnotherInterface interfaces must be an Array or a callable which returns an Array.' + ); + $objType->getInterfaces(); + } + private function schemaWithFieldType($type) { $schema = new Schema([ @@ -1220,11 +1405,11 @@ public function testAcceptsAScalarTypeDefiningParseValueAndParseLiteral() : void $this->schemaWithFieldType( new CustomScalarType([ 'name' => 'SomeScalar', - 'serialize' => static function () { + 'serialize' => static function () : void { }, - 'parseValue' => static function () { + 'parseValue' => static function () : void { }, - 'parseLiteral' => static function () { + 'parseLiteral' => static function () : void { }, ]) ); @@ -1242,9 +1427,9 @@ public function testRejectsAScalarTypeDefiningParseValueButNotParseLiteral() : v $this->schemaWithFieldType( new CustomScalarType([ 'name' => 'SomeScalar', - 'serialize' => static function () { + 'serialize' => static function () : void { }, - 'parseValue' => static function () { + 'parseValue' => static function () : void { }, ]) ); @@ -1262,9 +1447,9 @@ public function testRejectsAScalarTypeDefiningParseLiteralButNotParseValue() : v $this->schemaWithFieldType( new CustomScalarType([ 'name' => 'SomeScalar', - 'serialize' => static function () { + 'serialize' => static function () : void { }, - 'parseLiteral' => static function () { + 'parseLiteral' => static function () : void { }, ]) ); @@ -1282,7 +1467,7 @@ public function testRejectsAScalarTypeDefiningParseValueAndParseLiteralWithAnInc $this->schemaWithFieldType( new CustomScalarType([ 'name' => 'SomeScalar', - 'serialize' => static function () { + 'serialize' => static function () : void { }, 'parseValue' => new stdClass(), 'parseLiteral' => new stdClass(), @@ -1351,7 +1536,7 @@ public function testAcceptsAUnionTypeWithFunctionReturningAnArrayOfTypes() : voi $this->schemaWithFieldType( new UnionType([ 'name' => 'SomeUnion', - 'types' => function () { + 'types' => function () : array { return [$this->objectType]; }, ]) @@ -1413,7 +1598,7 @@ public function testAcceptsAnInputObjectTypeWithAFieldFunction() : void { $inputObjType = new InputObjectType([ 'name' => 'SomeInputObject', - 'fields' => static function () { + 'fields' => static function () : array { return [ 'f' => ['type' => Type::string()], ]; @@ -1423,6 +1608,23 @@ public function testAcceptsAnInputObjectTypeWithAFieldFunction() : void self::assertSame(Type::string(), $inputObjType->getField('f')->getType()); } + /** + * @see it('accepts an Input Object type with a field type function') + */ + public function testAcceptsAnInputObjectTypeWithAFieldTypeFunction() : void + { + $inputObjType = new InputObjectType([ + 'name' => 'SomeInputObject', + 'fields' => [ + 'f' => static function () : Type { + return Type::string(); + }, + ], + ]); + $inputObjType->assertValid(); + self::assertSame(Type::string(), $inputObjType->getField('f')->getType()); + } + /** * @see it('rejects an Input Object type with incorrect fields') */ @@ -1447,7 +1649,7 @@ public function testRejectsAnInputObjectTypeWithFieldsFunctionThatReturnsIncorre { $inputObjType = new InputObjectType([ 'name' => 'SomeInputObject', - 'fields' => static function () { + 'fields' => static function () : array { return []; }, ]); @@ -1469,7 +1671,7 @@ public function testRejectsAnInputObjectTypeWithResolvers() : void 'fields' => [ 'f' => [ 'type' => Type::string(), - 'resolve' => static function () { + 'resolve' => static function () : int { return 0; }, ], @@ -1477,7 +1679,7 @@ public function testRejectsAnInputObjectTypeWithResolvers() : void ]); $this->expectException(InvariantViolation::class); $this->expectExceptionMessage( - 'SomeInputObject.f field type has a resolve property, ' . + 'SomeInputObject.f field has a resolve property, ' . 'but Input Types cannot define resolvers.' ); $inputObjType->assertValid(); @@ -1501,7 +1703,7 @@ public function testRejectsAnInputObjectTypeWithResolverConstant() : void ]); $this->expectException(InvariantViolation::class); $this->expectExceptionMessage( - 'SomeInputObject.f field type has a resolve property, ' . + 'SomeInputObject.f field has a resolve property, ' . 'but Input Types cannot define resolvers.' ); $inputObjType->assertValid(); @@ -1576,84 +1778,6 @@ public function testDoesNotAllowIsDeprecatedWithoutDeprecationReasonOnEnum() : v $enumType->assertValid(); } - /** - * Type System: List must accept only types - */ - public function testListMustAcceptOnlyTypes() : void - { - $types = [ - Type::string(), - $this->scalarType, - $this->objectType, - $this->unionType, - $this->interfaceType, - $this->enumType, - $this->inputObjectType, - Type::listOf(Type::string()), - Type::nonNull(Type::string()), - ]; - - $badTypes = [[], new stdClass(), '', null]; - - foreach ($types as $type) { - try { - Type::listOf($type); - } catch (Throwable $e) { - self::fail('List is expected to accept type: ' . get_class($type) . ', but got error: ' . $e->getMessage()); - } - } - foreach ($badTypes as $badType) { - $typeStr = Utils::printSafe($badType); - try { - Type::listOf($badType); - self::fail(sprintf('List should not accept %s', $typeStr)); - } catch (InvariantViolation $e) { - self::assertEquals(sprintf('Expected %s to be a GraphQL type.', $typeStr), $e->getMessage()); - } - } - } - - /** - * Type System: NonNull must only accept non-nullable types - */ - public function testNonNullMustOnlyAcceptNonNullableTypes() : void - { - $nullableTypes = [ - Type::string(), - $this->scalarType, - $this->objectType, - $this->unionType, - $this->interfaceType, - $this->enumType, - $this->inputObjectType, - Type::listOf(Type::string()), - Type::listOf(Type::nonNull(Type::string())), - ]; - $notNullableTypes = [ - Type::nonNull(Type::string()), - [], - new stdClass(), - '', - null, - ]; - foreach ($nullableTypes as $type) { - try { - Type::nonNull($type); - } catch (Throwable $e) { - self::fail('NonNull is expected to accept type: ' . get_class($type) . ', but got error: ' . $e->getMessage()); - } - } - foreach ($notNullableTypes as $badType) { - $typeStr = Utils::printSafe($badType); - try { - Type::nonNull($badType); - self::fail(sprintf('Nulls should not accept %s', $typeStr)); - } catch (InvariantViolation $e) { - self::assertEquals(sprintf('Expected %s to be a GraphQL nullable type.', $typeStr), $e->getMessage()); - } - } - } - /** * @see it('rejects a Schema which redefines a built-in type') */ @@ -1661,7 +1785,7 @@ public function testRejectsASchemaWhichRedefinesABuiltInType() : void { $FakeString = new CustomScalarType([ 'name' => 'String', - 'serialize' => static function () { + 'serialize' => static function () : void { }, ]); diff --git a/tests/Type/DirectiveTest.php b/tests/Type/DirectiveTest.php new file mode 100644 index 000000000..c98265379 --- /dev/null +++ b/tests/Type/DirectiveTest.php @@ -0,0 +1,15 @@ + { + */ +final class DirectiveTest extends TestCase +{ + // TODO implement https://github.com/graphql/graphql-js/blob/master/src/type/__tests__/directive-test.js +} diff --git a/tests/Type/EnumTypeTest.php b/tests/Type/EnumTypeTest.php index 9b2e8e62d..e573e51c4 100644 --- a/tests/Type/EnumTypeTest.php +++ b/tests/Type/EnumTypeTest.php @@ -5,8 +5,10 @@ namespace GraphQL\Tests\Type; use ArrayObject; +use GraphQL\Error\DebugFlag; use GraphQL\GraphQL; use GraphQL\Language\SourceLocation; +use GraphQL\Tests\PHPUnit\ArraySubsetAsserts; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; @@ -18,6 +20,8 @@ class EnumTypeTest extends TestCase { + use ArraySubsetAsserts; + /** @var Schema */ private $schema; @@ -30,7 +34,7 @@ class EnumTypeTest extends TestCase /** @var ArrayObject */ private $Complex2; - public function setUp() + public function setUp() : void { $ColorType = new EnumType([ 'name' => 'Color', @@ -51,7 +55,7 @@ public function setUp() ]); $Complex1 = [ - 'someRandomFunction' => static function () { + 'someRandomFunction' => static function () : void { }, ]; $Complex2 = new ArrayObject(['someRandomValue' => 123]); @@ -64,6 +68,15 @@ public function setUp() ], ]); + $Array1 = ['one', 'ONE']; + $ArrayValuesEnum = new EnumType([ + 'name' => 'ArrayValuesEnum', + 'values' => [ + 'ONE' => ['value' => $Array1], + 'TWO' => ['value' => ['two', 'TWO']], + ], + ]); + $QueryType = new ObjectType([ 'name' => 'Query', 'fields' => [ @@ -74,7 +87,7 @@ public function setUp() 'fromInt' => ['type' => Type::int()], 'fromString' => ['type' => Type::string()], ], - 'resolve' => static function ($value, $args) { + 'resolve' => static function ($rootValue, $args) { if (isset($args['fromInt'])) { return $args['fromInt']; } @@ -92,7 +105,7 @@ public function setUp() 'fromName' => ['type' => Type::string()], 'fromValue' => ['type' => Type::string()], ], - 'resolve' => static function ($value, $args) { + 'resolve' => static function ($rootValue, $args) { if (isset($args['fromName'])) { return $args['fromName']; } @@ -107,7 +120,7 @@ public function setUp() 'fromEnum' => ['type' => $ColorType], 'fromInt' => ['type' => Type::int()], ], - 'resolve' => static function ($value, $args) { + 'resolve' => static function ($rootValue, $args) { if (isset($args['fromInt'])) { return $args['fromInt']; } @@ -132,18 +145,45 @@ public function setUp() 'type' => Type::boolean(), ], ], - 'resolve' => static function ($value, $args) use ($Complex2) { - if (! empty($args['provideGoodValue'])) { + 'resolve' => static function ($rootValue, $args) use ($Complex2) { + if ($args['provideGoodValue'] ?? false) { // Note: this is one of the references of the internal values which // ComplexEnum allows. return $Complex2; } - if (! empty($args['provideBadValue'])) { + if ($args['provideBadValue'] ?? false) { // Note: similar shape, but not the same *reference* // as Complex2 above. Enum internal values require === equality. return new ArrayObject(['someRandomValue' => 123]); } + return $args['fromEnum']; + }, + ], + 'arrayValuesEnum' => [ + 'type' => $ArrayValuesEnum, + 'args' => [ + 'fromEnum' => [ + 'type' => $ArrayValuesEnum, + // Note: defaultValue is provided an *internal* representation for + // Enums, rather than the string name. + 'defaultValue' => $Array1, + ], + 'provideOneByReference' => [ + 'type' => Type::boolean(), + ], + 'provideTwo' => [ + 'type' => Type::boolean(), + ], + ], + 'resolve' => static function ($rootValue, $args) use (&$Array1) { + if ($args['provideOneByReference'] ?? false) { + return $Array1; + } + if ($args['provideTwo'] ?? false) { + return ['two', 'TWO']; + } + return $args['fromEnum']; }, ], @@ -156,7 +196,7 @@ public function setUp() 'favoriteEnum' => [ 'type' => $ColorType, 'args' => ['color' => ['type' => $ColorType]], - 'resolve' => static function ($value, $args) { + 'resolve' => static function ($rootValue, $args) { return $args['color'] ?? null; }, ], @@ -169,7 +209,7 @@ public function setUp() 'subscribeToEnum' => [ 'type' => $ColorType, 'args' => ['color' => ['type' => $ColorType]], - 'resolve' => static function ($value, $args) { + 'resolve' => static function ($rootValue, $args) { return $args['color'] ?? null; }, ], @@ -240,7 +280,7 @@ public function testDoesNotAcceptStringLiterals() : void private function expectFailure($query, $vars, $err) { $result = GraphQL::executeQuery($this->schema, $query, null, null, $vars); - self::assertEquals(1, count($result->errors)); + self::assertCount(1, $result->errors); if (is_array($err)) { self::assertEquals( @@ -493,7 +533,7 @@ public function testMayBeInternallyRepresentedWithComplexValues() : void good: complexEnum(provideGoodValue: true) bad: complexEnum(provideBadValue: true) }' - )->toArray(true); + )->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE); $expected = [ 'data' => [ @@ -513,6 +553,30 @@ public function testMayBeInternallyRepresentedWithComplexValues() : void self::assertArraySubset($expected, $result); } + public function testMayBeInternallyRepresentedWithArrayValues() : void + { + $result = GraphQL::executeQuery( + $this->schema, + '{ + defaultValue: arrayValuesEnum + fromName: arrayValuesEnum(fromEnum: TWO) + oneRef: arrayValuesEnum(provideOneByReference: true) + two: arrayValuesEnum(provideTwo: true) + }' + )->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE); + + $expected = [ + 'data' => [ + 'defaultValue' => 'ONE', + 'fromName' => 'TWO', + 'oneRef' => 'ONE', + 'two' => 'TWO', + ], + ]; + + self::assertEquals($expected, $result); + } + /** * @see it('can be introspected without error') */ @@ -539,7 +603,7 @@ public function testAllowsSimpleArrayAsValues() : void ], ], ], - GraphQL::executeQuery($this->schema, $q)->toArray(true) + GraphQL::executeQuery($this->schema, $q)->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE) ); } } diff --git a/tests/Type/IntrospectionTest.php b/tests/Type/IntrospectionTest.php index 51feb401b..39ae4ea35 100644 --- a/tests/Type/IntrospectionTest.php +++ b/tests/Type/IntrospectionTest.php @@ -7,18 +7,22 @@ use GraphQL\Error\FormattedError; use GraphQL\GraphQL; use GraphQL\Language\SourceLocation; +use GraphQL\Tests\PHPUnit\ArraySubsetAsserts; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Introspection; use GraphQL\Type\Schema; -use GraphQL\Validator\Rules\ProvidedNonNullArguments; +use GraphQL\Validator\Rules\ProvidedRequiredArguments; use PHPUnit\Framework\TestCase; use function json_encode; +use function sprintf; class IntrospectionTest extends TestCase { + use ArraySubsetAsserts; + /** * @see it('executes an introspection query') */ @@ -31,7 +35,10 @@ public function testExecutesAnIntrospectionQuery() : void ]), ]); - $request = Introspection::getIntrospectionQuery(['descriptions' => false]); + $request = Introspection::getIntrospectionQuery([ + 'descriptions' => false, + 'directiveIsRepeatable' => true, + ]); $expected = [ 'data' => [ @@ -790,7 +797,7 @@ public function testExecutesAnIntrospectionQuery() : void ], 2 => [ - 'name' => 'locations', + 'name' => 'args', 'args' => [], 'type' => @@ -807,8 +814,8 @@ public function testExecutesAnIntrospectionQuery() : void 'name' => null, 'ofType' => [ - 'kind' => 'ENUM', - 'name' => '__DirectiveLocation', + 'kind' => 'OBJECT', + 'name' => '__InputValue', ], ], ], @@ -818,7 +825,25 @@ public function testExecutesAnIntrospectionQuery() : void ], 3 => [ - 'name' => 'args', + 'name' => 'isRepeatable', + 'args' => + [], + 'type' => + [ + 'kind' => 'NON_NULL', + 'name' => null, + 'ofType' => [ + 'kind' => 'SCALAR', + 'name' => 'Boolean', + 'ofType' => null, + ], + ], + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + 4 => + [ + 'name' => 'locations', 'args' => [], 'type' => @@ -835,8 +860,8 @@ public function testExecutesAnIntrospectionQuery() : void 'name' => null, 'ofType' => [ - 'kind' => 'OBJECT', - 'name' => '__InputValue', + 'kind' => 'ENUM', + 'name' => '__DirectiveLocation', ], ], ], @@ -844,61 +869,6 @@ public function testExecutesAnIntrospectionQuery() : void 'isDeprecated' => false, 'deprecationReason' => null, ], - 4 => - [ - 'name' => 'onOperation', - 'args' => - [], - 'type' => - [ - 'kind' => 'NON_NULL', - 'name' => null, - 'ofType' => - [ - 'kind' => 'SCALAR', - 'name' => 'Boolean', - ], - ], - 'isDeprecated' => true, - 'deprecationReason' => 'Use `locations`.', - ], - 5 => - [ - 'name' => 'onFragment', - 'args' => - [], - 'type' => - [ - 'kind' => 'NON_NULL', - 'name' => null, - 'ofType' => - [ - 'kind' => 'SCALAR', - 'name' => 'Boolean', - ], - ], - 'isDeprecated' => true, - 'deprecationReason' => 'Use `locations`.', - ], - 6 => - [ - 'name' => 'onField', - 'args' => - [], - 'type' => - [ - 'kind' => 'NON_NULL', - 'name' => null, - 'ofType' => - [ - 'kind' => 'SCALAR', - 'name' => 'Boolean', - - ], - ], - 'isDeprecated' => true, - 'deprecationReason' => 'Use `locations`.', - ], ], 'inputFields' => null, 'interfaces' => @@ -956,6 +926,67 @@ public function testExecutesAnIntrospectionQuery() : void 'isDeprecated' => false, 'deprecationReason' => null, ], + 7 => + [ + 'name' => 'VARIABLE_DEFINITION', + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + [ + 'name' => 'SCHEMA', + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + [ + 'name' => 'SCALAR', + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + [ + 'name' => 'OBJECT', + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + [ + 'name' => 'FIELD_DEFINITION', + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + [ + 'name' => 'ARGUMENT_DEFINITION', + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + [ + 'name' => 'INTERFACE', + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + [ + 'name' => 'UNION', + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + [ + 'name' => 'ENUM', + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + [ + 'name' => 'ENUM_VALUE', + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + [ + 'name' => 'INPUT_OBJECT', + 'isDeprecated' => false, + 'deprecationReason' => null, + ], + [ + 'name' => 'INPUT_FIELD_DEFINITION', + 'isDeprecated' => false, + 'deprecationReason' => null, + ], ], 'possibleTypes' => null, ], @@ -965,12 +996,7 @@ public function testExecutesAnIntrospectionQuery() : void 0 => [ 'name' => 'include', - 'locations' => - [ - 0 => 'FIELD', - 1 => 'FRAGMENT_SPREAD', - 2 => 'INLINE_FRAGMENT', - ], + 'isRepeatable' => false, 'args' => [ 0 => @@ -989,16 +1015,17 @@ public function testExecutesAnIntrospectionQuery() : void ], ], ], - ], - 1 => - [ - 'name' => 'skip', 'locations' => [ 0 => 'FIELD', 1 => 'FRAGMENT_SPREAD', 2 => 'INLINE_FRAGMENT', ], + ], + 1 => + [ + 'name' => 'skip', + 'isRepeatable' => false, 'args' => [ 0 => @@ -1017,6 +1044,36 @@ public function testExecutesAnIntrospectionQuery() : void ], ], ], + 'locations' => + [ + 0 => 'FIELD', + 1 => 'FRAGMENT_SPREAD', + 2 => 'INLINE_FRAGMENT', + ], + ], + 2 => + [ + 'name' => 'deprecated', + 'isRepeatable' => false, + 'args' => + [ + 0 => + [ + 'defaultValue' => '"No longer supported"', + 'name' => 'reason', + 'type' => + [ + 'kind' => 'SCALAR', + 'name' => 'String', + 'ofType' => null, + ], + ], + ], + 'locations' => + [ + 0 => 'FIELD_DEFINITION', + 1 => 'ENUM_VALUE', + ], ], ], ], @@ -1049,7 +1106,7 @@ public function testIntrospectsOnInputObject() : void 'field' => [ 'type' => Type::string(), 'args' => ['complex' => ['type' => $TestInputObject]], - 'resolve' => static function ($_, $args) { + 'resolve' => static function ($testType, $args) { return json_encode($args['complex']); }, ], @@ -1418,7 +1475,7 @@ public function testFailsAsExpectedOnTheTypeRootFieldWithoutAnArg() : void $expected = [ 'errors' => [ FormattedError::create( - ProvidedNonNullArguments::missingFieldArgMessage('__type', 'name', 'String!'), + ProvidedRequiredArguments::missingFieldArgMessage('__type', 'name', 'String!'), [new SourceLocation(3, 9)] ), ], @@ -1526,7 +1583,7 @@ enumValues { ], [ 'description' => 'Indicates this type is an interface. ' . - '`fields` and `possibleTypes` are valid fields.', + '`fields`, `interfaces`, and `possibleTypes` are valid fields.', 'name' => 'INTERFACE', ], [ @@ -1561,4 +1618,31 @@ enumValues { self::assertEquals($expected, GraphQL::executeQuery($schema, $request)->toArray()); } + + /** + * @see it('executes an introspection query without calling global fieldResolver') + */ + public function testExecutesAnIntrospectionQueryWithoutCallingGlobalFieldResolver() + { + $QueryRoot = new ObjectType([ + 'name' => 'QueryRoot', + 'fields' => [ + 'onlyField' => [ 'type' => Type::string() ], + ], + ]); + + $schema = new Schema([ 'query' => $QueryRoot ]); + $source = Introspection::getIntrospectionQuery(['directiveIsRepeatable' => true]); + + $calledForFields = []; + /* istanbul ignore next */ + $fieldResolver = static function ($value, $_1, $_2, $info) use (&$calledForFields) { + $calledForFields[sprintf('%s::%s', $info->parentType->name, $info->fieldName)] = true; + + return $value; + }; + + GraphQL::executeQuery($schema, $source, null, null, null, null, $fieldResolver); + self::assertEmpty($calledForFields); + } } diff --git a/tests/Type/LazyTypeLoaderTest.php b/tests/Type/LazyTypeLoaderTest.php new file mode 100644 index 000000000..1e208f994 --- /dev/null +++ b/tests/Type/LazyTypeLoaderTest.php @@ -0,0 +1,392 @@ +loadedTypes[$name])) { + $type = null; + switch ($name) { + case 'Node': + $type = new InterfaceType([ + 'name' => 'Node', + 'fields' => function () : array { + $this->calls[] = 'Node.fields'; + + return [ + 'id' => Type::string(), + ]; + }, + 'resolveType' => static function () : void { + }, + ]); + break; + + case 'Content': + $type = new InterfaceType([ + 'name' => 'Content', + 'fields' => function () : array { + $this->calls[] = 'Content.fields'; + + return [ + 'title' => Type::string(), + 'body' => Type::string(), + ]; + }, + 'resolveType' => static function () : void { + }, + ]); + break; + + case 'BlogStory': + $type = new ObjectType([ + 'name' => 'BlogStory', + 'interfaces' => [ + $this->node, + $this->content, + ], + 'fields' => function () : array { + $this->calls[] = 'BlogStory.fields'; + + return [ + 'id' => Type::string(), + 'title' => Type::string(), + 'body' => Type::string(), + ]; + }, + ]); + break; + + case 'PostStoryMutation': + $type = new ObjectType([ + 'name' => 'PostStoryMutation', + 'fields' => [ + 'story' => $this->blogStory, + ], + ]); + break; + + case 'PostStoryMutationInput': + $type = new InputObjectType([ + 'name' => 'PostStoryMutationInput', + 'fields' => [ + 'title' => Type::string(), + 'body' => Type::string(), + 'author' => Type::id(), + 'category' => Type::id(), + ], + ]); + break; + } + $this->loadedTypes[$name] = $type; + } + + return $this->loadedTypes[$name]; + }; + } + + public function setUp() : void + { + $this->calls = []; + + $this->node = $this->lazyLoad('Node'); + $this->blogStory = $this->lazyLoad('BlogStory'); + $this->content = $this->lazyLoad('Content'); + $this->postStoryMutation = $this->lazyLoad('PostStoryMutation'); + $this->postStoryMutationInput = $this->lazyLoad('PostStoryMutationInput'); + $this->query = new ObjectType([ + 'name' => 'Query', + 'fields' => function () : array { + $this->calls[] = 'Query.fields'; + + return [ + 'latestContent' => $this->lazyLoad('Content'), + 'node' => $this->lazyLoad('Node'), + ]; + }, + ]); + + $this->mutation = new ObjectType([ + 'name' => 'Mutation', + 'fields' => function () : array { + $this->calls[] = 'Mutation.fields'; + + return [ + 'postStory' => [ + 'type' => $this->postStoryMutation, + 'args' => [ + 'input' => Type::nonNull($this->postStoryMutationInput), + 'clientRequestId' => Type::string(), + ], + ], + ]; + }, + ]); + + $this->typeLoader = function (string $name) { + $this->calls[] = $name; + $prop = lcfirst($name); + + switch ($prop) { + case 'node': + return ($this->node)(); + case 'blogStory': + return ($this->blogStory)(); + case 'content': + return $this->content; + case 'postStoryMutation': + return $this->postStoryMutation; + case 'postStoryMutationInput': + return $this->postStoryMutationInput; + } + + return null; + }; + } + + public function testSchemaAcceptsTypeLoader() : void + { + $this->expectNotToPerformAssertions(); + new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => ['a' => Type::string()], + ]), + 'typeLoader' => static function () : void { + }, + ]); + } + + public function testSchemaRejectsNonCallableTypeLoader() : void + { + $this->expectException(InvariantViolation::class); + $this->expectExceptionMessage('Schema type loader must be callable if provided but got: []'); + + new Schema([ + 'query' => new ObjectType([ + 'name' => 'Query', + 'fields' => ['a' => Type::string()], + ]), + 'typeLoader' => [], + ]); + } + + public function testWorksWithoutTypeLoader() : void + { + $schema = new Schema([ + 'query' => $this->query, + 'mutation' => $this->mutation, + 'types' => [Schema::resolveType($this->blogStory)], + ]); + + $expected = [ + 'Query.fields', + 'Content.fields', + 'Node.fields', + 'Mutation.fields', + 'BlogStory.fields', + ]; + self::assertEquals($expected, $this->calls); + + self::assertSame($this->query, $schema->getType('Query')); + self::assertSame($this->mutation, $schema->getType('Mutation')); + self::assertSame(Schema::resolveType($this->node), $schema->getType('Node')); + self::assertSame(Schema::resolveType($this->content), $schema->getType('Content')); + self::assertSame(Schema::resolveType($this->blogStory), $schema->getType('BlogStory')); + self::assertSame(Schema::resolveType($this->postStoryMutation), $schema->getType('PostStoryMutation')); + self::assertSame(Schema::resolveType($this->postStoryMutationInput), $schema->getType('PostStoryMutationInput')); + + $expectedTypeMap = [ + 'Query' => $this->query, + 'Mutation' => $this->mutation, + 'Node' => Schema::resolveType($this->node), + 'String' => Type::string(), + 'Content' => Schema::resolveType($this->content), + 'BlogStory' => Schema::resolveType($this->blogStory), + 'PostStoryMutationInput' => Schema::resolveType($this->postStoryMutationInput), + ]; + + self::assertArraySubset($expectedTypeMap, $schema->getTypeMap()); + } + + public function testWorksWithTypeLoader() : void + { + $schema = new Schema([ + 'query' => $this->query, + 'mutation' => $this->mutation, + 'typeLoader' => $this->typeLoader, + ]); + self::assertEquals([], $this->calls); + + $node = $schema->getType('Node'); + self::assertSame(Schema::resolveType($this->node), $node); + self::assertEquals(['Node'], $this->calls); + + $content = $schema->getType('Content'); + self::assertSame(Schema::resolveType($this->content), $content); + self::assertEquals(['Node', 'Content'], $this->calls); + + $input = $schema->getType('PostStoryMutationInput'); + self::assertSame(Schema::resolveType($this->postStoryMutationInput), $input); + self::assertEquals(['Node', 'Content', 'PostStoryMutationInput'], $this->calls); + + $result = $schema->isPossibleType( + Schema::resolveType($this->node), + Schema::resolveType($this->blogStory) + ); + self::assertTrue($result); + self::assertEquals( + [ + 'Node', + 'Content', + 'PostStoryMutationInput', + 'Query.fields', + 'Content.fields', + 'Node.fields', + 'Mutation.fields', + 'BlogStory.fields', + ], + $this->calls + ); + } + + public function testOnlyCallsLoaderOnce() : void + { + $schema = new Schema([ + 'query' => $this->query, + 'typeLoader' => $this->typeLoader, + ]); + + $schema->getType('Node'); + self::assertEquals(['Node'], $this->calls); + + $schema->getType('Node'); + self::assertEquals(['Node'], $this->calls); + } + + public function testFailsOnNonExistentType() : void + { + $schema = new Schema([ + 'query' => $this->query, + 'typeLoader' => static function () : void { + }, + ]); + + $this->expectException(InvariantViolation::class); + $this->expectExceptionMessage('Type loader is expected to return a callable or valid type "NonExistingType", but it returned null'); + + $schema->getType('NonExistingType'); + } + + public function testFailsOnNonType() : void + { + $schema = new Schema([ + 'query' => $this->query, + 'typeLoader' => static function () : stdClass { + return new stdClass(); + }, + ]); + + $this->expectException(InvariantViolation::class); + $this->expectExceptionMessage('Type loader is expected to return a callable or valid type "Node", but it returned instance of stdClass'); + + $schema->getType('Node'); + } + + public function testPassesThroughAnExceptionInLoader() : void + { + $schema = new Schema([ + 'query' => $this->query, + 'typeLoader' => static function () : void { + throw new Exception('This is the exception we are looking for'); + }, + ]); + + $this->expectException(Throwable::class); + $this->expectExceptionMessage('This is the exception we are looking for'); + + $schema->getType('Node'); + } + + public function testReturnsIdenticalResults() : void + { + $withoutLoader = new Schema([ + 'query' => $this->query, + 'mutation' => $this->mutation, + ]); + + $withLoader = new Schema([ + 'query' => $this->query, + 'mutation' => $this->mutation, + 'typeLoader' => $this->typeLoader, + ]); + + self::assertSame($withoutLoader->getQueryType(), $withLoader->getQueryType()); + self::assertSame($withoutLoader->getMutationType(), $withLoader->getMutationType()); + self::assertSame($withoutLoader->getType('BlogStory'), $withLoader->getType('BlogStory')); + self::assertSame($withoutLoader->getDirectives(), $withLoader->getDirectives()); + } + + public function testSkipsLoaderForInternalTypes() : void + { + $schema = new Schema([ + 'query' => $this->query, + 'mutation' => $this->mutation, + 'typeLoader' => $this->typeLoader, + ]); + + $type = $schema->getType('ID'); + self::assertSame(Type::id(), $type); + self::assertEquals([], $this->calls); + } +} diff --git a/tests/Type/QueryPlanTest.php b/tests/Type/QueryPlanTest.php index 50ec6ae95..e97dc0ec3 100644 --- a/tests/Type/QueryPlanTest.php +++ b/tests/Type/QueryPlanTest.php @@ -5,10 +5,13 @@ namespace GraphQL\Tests\Type; use GraphQL\GraphQL; +use GraphQL\Tests\Executor\TestClasses\Dog; +use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\QueryPlan; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; +use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Schema; use PHPUnit\Framework\TestCase; @@ -29,7 +32,7 @@ public function testQueryPlan() : void $author = new ObjectType([ 'name' => 'Author', - 'fields' => static function () use ($image, &$article) { + 'fields' => static function () use ($image, &$article) : array { return [ 'id' => ['type' => Type::string()], 'name' => ['type' => Type::string()], @@ -295,6 +298,171 @@ public function testQueryPlan() : void self::assertFalse($queryPlan->hasType('Test')); } + public function testQueryPlanForWrappedTypes() : void + { + $article = new ObjectType([ + 'name' => 'Article', + 'fields' => [ + 'id' => ['type' => Type::string()], + ], + ]); + + $doc = ' + query Test { + articles { + id + } + } +'; + $expectedQueryPlan = [ + 'id' => [ + 'type' => Type::string(), + 'fields' => [], + 'args' => [], + ], + ]; + + /** @var QueryPlan|null $queryPlan */ + $queryPlan = null; + + $blogQuery = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'articles' => [ + 'type' => Type::nonNull(Type::listOf($article)), + 'resolve' => static function ( + $value, + $args, + $context, + ResolveInfo $info + ) use ( + &$queryPlan + ) : array { + $queryPlan = $info->lookAhead(); + + return []; + }, + ], + ], + ]); + + $schema = new Schema(['query' => $blogQuery]); + GraphQL::executeQuery($schema, $doc); + + self::assertSame($expectedQueryPlan, $queryPlan->queryPlan()); + } + + public function testQueryPlanOnInterface() : void + { + $petType = new InterfaceType([ + 'name' => 'Pet', + 'fields' => static function () : array { + return [ + 'name' => ['type' => Type::string()], + ]; + }, + ]); + + $dogType = new ObjectType([ + 'name' => 'Dog', + 'interfaces' => [$petType], + 'isTypeOf' => static function ($obj) : bool { + return $obj instanceof Dog; + }, + 'fields' => static function () : array { + return [ + 'name' => ['type' => Type::string()], + 'woofs' => ['type' => Type::boolean()], + ]; + }, + ]); + + $query = 'query Test { + pets { + name + ... on Dog { + woofs + } + } + }'; + + $expectedQueryPlan = [ + 'woofs' => [ + 'type' => Type::boolean(), + 'fields' => [], + 'args' => [], + ], + 'name' => [ + 'type' => Type::string(), + 'args' => [], + 'fields' => [], + ], + ]; + + $expectedReferencedTypes = [ + 'Dog', + 'Pet', + ]; + + $expectedReferencedFields = [ + 'woofs', + 'name', + ]; + + /** @var QueryPlan $queryPlan */ + $queryPlan = null; + $hasCalled = false; + + $petsQuery = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'pets' => [ + 'type' => Type::listOf($petType), + 'resolve' => static function ( + $value, + $args, + $context, + ResolveInfo $info + ) use ( + &$hasCalled, + &$queryPlan + ) : array { + $hasCalled = true; + $queryPlan = $info->lookAhead(); + + return []; + }, + ], + ], + ]); + + $schema = new Schema([ + 'query' => $petsQuery, + 'types' => [$dogType], + 'typeLoader' => static function ($name) use ($dogType, $petType) { + switch ($name) { + case 'Dog': + return $dogType; + case 'Pet': + return $petType; + } + }, + ]); + GraphQL::executeQuery($schema, $query)->toArray(); + + self::assertTrue($hasCalled); + self::assertEquals($expectedQueryPlan, $queryPlan->queryPlan()); + self::assertEquals($expectedReferencedTypes, $queryPlan->getReferencedTypes()); + self::assertEquals($expectedReferencedFields, $queryPlan->getReferencedFields()); + self::assertEquals(['woofs'], $queryPlan->subFields('Dog')); + + self::assertTrue($queryPlan->hasField('name')); + self::assertFalse($queryPlan->hasField('test')); + + self::assertTrue($queryPlan->hasType('Dog')); + self::assertFalse($queryPlan->hasType('Test')); + } + public function testMergedFragmentsQueryPlan() : void { $image = new ObjectType([ @@ -310,7 +478,7 @@ public function testMergedFragmentsQueryPlan() : void $author = new ObjectType([ 'name' => 'Author', - 'fields' => static function () use ($image, &$article) { + 'fields' => static function () use ($image, &$article) : array { return [ 'id' => ['type' => Type::string()], 'name' => ['type' => Type::string()], @@ -585,4 +753,223 @@ public function testMergedFragmentsQueryPlan() : void self::assertTrue($queryPlan->hasType('Image')); self::assertFalse($queryPlan->hasType('Test')); } + + public function testQueryPlanGroupingImplementorFieldsForAbstractTypes() : void + { + $car = null; + + $item = new InterfaceType([ + 'name' => 'Item', + 'fields' => [ + 'id' => Type::int(), + 'owner' => Type::string(), + ], + 'resolveType' => static function () use (&$car) { + return $car; + }, + ]); + + $manualTransmission = new ObjectType([ + 'name' => 'ManualTransmission', + 'fields' => [ + 'speed' => Type::int(), + 'overdrive' => Type::boolean(), + ], + ]); + + $automaticTransmission = new ObjectType([ + 'name' => 'AutomaticTransmission', + 'fields' => [ + 'speed' => Type::int(), + 'sportMode' => Type::boolean(), + ], + ]); + + $transmission = new UnionType([ + 'name' => 'Transmission', + 'types' => [$manualTransmission, $automaticTransmission], + 'resolveType' => static function () use ($manualTransmission) : ObjectType { + return $manualTransmission; + }, + ]); + + $car = new ObjectType([ + 'name' => 'Car', + 'fields' => [ + 'id' => Type::int(), + 'owner' => Type::string(), + 'mark' => Type::string(), + 'model' => Type::string(), + 'transmission' => $transmission, + ], + 'interfaces' => [$item], + ]); + + $building = new ObjectType([ + 'name' => 'Building', + 'fields' => [ + 'id' => Type::int(), + 'owner' => Type::string(), + 'city' => Type::string(), + 'address' => Type::string(), + ], + 'interfaces' => [$item], + ]); + + $query = '{ + item { + id + owner + ... on Car { + mark + model + transmission { + ... on ManualTransmission { + speed + overdrive + } + ... on AutomaticTransmission { + speed + sportMode + } + } + } + ... on Building { + city + } + ...BuildingFragment + } + } + fragment BuildingFragment on Building { + address + }'; + + $expectedResult = [ + 'data' => ['item' => null], + ]; + + $expectedQueryPlan = [ + 'fields' => [ + 'id' => [ + 'type' => Type::int(), + 'fields' => [], + 'args' => [], + ], + 'owner' => [ + 'type' => Type::string(), + 'fields' => [], + 'args' => [], + ], + ], + 'implementors' => [ + 'Car' => [ + 'type' => $car, + 'fields' => [ + 'mark' => [ + 'type' => Type::string(), + 'fields' => [], + 'args' => [], + ], + 'model' => [ + 'type' => Type::string(), + 'fields' => [], + 'args' => [], + ], + 'transmission' => [ + 'type' => $transmission, + 'fields' => [], + 'args' => [], + 'implementors' => [ + 'ManualTransmission' => [ + 'type' => $manualTransmission, + 'fields' => [ + 'speed' => [ + 'type' => Type::int(), + 'fields' => [], + 'args' => [], + ], + 'overdrive' => [ + 'type' => Type::boolean(), + 'fields' => [], + 'args' => [], + ], + ], + ], + 'AutomaticTransmission' => [ + 'type' => $automaticTransmission, + 'fields' => [ + 'speed' => [ + 'type' => Type::int(), + 'fields' => [], + 'args' => [], + ], + 'sportMode' => [ + 'type' => Type::boolean(), + 'fields' => [], + 'args' => [], + ], + ], + ], + ], + ], + ], + ], + 'Building' => [ + 'type' => $building, + 'fields' => [ + 'city' => [ + 'type' => Type::string(), + 'fields' => [], + 'args' => [], + ], + 'address' => [ + 'type' => Type::string(), + 'fields' => [], + 'args' => [], + ], + ], + ], + ], + ]; + + $expectedReferencedTypes = ['ManualTransmission', 'AutomaticTransmission', 'Transmission', 'Car', 'Building', 'Item']; + + $expectedReferencedFields = ['speed', 'overdrive', 'sportMode', 'mark', 'model', 'transmission', 'city', 'address', 'id', 'owner']; + + $expectedItemSubFields = ['id', 'owner']; + $expectedBuildingSubFields = ['city', 'address']; + + $hasCalled = false; + /** @var QueryPlan $queryPlan */ + $queryPlan = null; + + $root = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'item' => [ + 'type' => $item, + 'resolve' => static function ($value, $args, $context, ResolveInfo $info) use (&$hasCalled, &$queryPlan) { + $hasCalled = true; + $queryPlan = $info->lookAhead(['group-implementor-fields']); + + return null; + }, + ], + ], + ]); + + $schema = new Schema([ + 'query' => $root, + 'types' => [$car, $building], + ]); + $result = GraphQL::executeQuery($schema, $query)->toArray(); + + self::assertTrue($hasCalled); + self::assertEquals($expectedResult, $result); + self::assertEquals($expectedQueryPlan, $queryPlan->queryPlan()); + self::assertEquals($expectedReferencedTypes, $queryPlan->getReferencedTypes()); + self::assertEquals($expectedReferencedFields, $queryPlan->getReferencedFields()); + self::assertEquals($expectedItemSubFields, $queryPlan->subFields('Item')); + self::assertEquals($expectedBuildingSubFields, $queryPlan->subFields('Building')); + } } diff --git a/tests/Type/ResolveInfoTest.php b/tests/Type/ResolveInfoTest.php index d228a82b8..000711f53 100644 --- a/tests/Type/ResolveInfoTest.php +++ b/tests/Type/ResolveInfoTest.php @@ -28,7 +28,7 @@ public function testFieldSelection() : void $author = new ObjectType([ 'name' => 'Author', - 'fields' => static function () use ($image, &$article) { + 'fields' => static function () use ($image, &$article) : array { return [ 'id' => ['type' => Type::string()], 'name' => ['type' => Type::string()], @@ -181,6 +181,34 @@ public function testFieldSelection() : void self::assertEquals($expectedDeepSelection, $actualDeepSelection); } + public function testFieldSelectionOnScalarTypes() : void + { + $query = ' + query Ping { + ping + } + '; + + $pingPongQuery = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'ping' => [ + 'type' => Type::string(), + 'resolve' => static function ($value, $args, $context, ResolveInfo $info) : string { + self::assertEquals([], $info->getFieldSelection()); + + return 'pong'; + }, + ], + ], + ]); + + $schema = new Schema(['query' => $pingPongQuery]); + $result = GraphQL::executeQuery($schema, $query)->toArray(); + + self::assertEquals(['data' => ['ping' => 'pong']], $result); + } + public function testMergedFragmentsFieldSelection() : void { $image = new ObjectType([ @@ -196,7 +224,7 @@ public function testMergedFragmentsFieldSelection() : void $author = new ObjectType([ 'name' => 'Author', - 'fields' => static function () use ($image, &$article) { + 'fields' => static function () use ($image, &$article) : array { return [ 'id' => ['type' => Type::string()], 'name' => ['type' => Type::string()], diff --git a/tests/Type/ScalarSerializationTest.php b/tests/Type/ScalarSerializationTest.php index 17f236843..7f6734aff 100644 --- a/tests/Type/ScalarSerializationTest.php +++ b/tests/Type/ScalarSerializationTest.php @@ -5,17 +5,22 @@ namespace GraphQL\Tests\Type; use GraphQL\Error\Error; +use GraphQL\Tests\Type\TestClasses\CanCastToString; +use GraphQL\Tests\Type\TestClasses\ObjectIdStub; use GraphQL\Type\Definition\Type; use PHPUnit\Framework\TestCase; use stdClass; +use function acos; +use function log; class ScalarSerializationTest extends TestCase { // Type System: Scalar coercion + /** - * @see it('serializes output int') + * @see it('serializes output as Int') */ - public function testSerializesOutputInt() : void + public function testSerializesOutputAsInt() : void { $intType = Type::int(); @@ -29,94 +34,44 @@ public function testSerializesOutputInt() : void self::assertSame(1, $intType->serialize(true)); } - public function testSerializesOutputIntCannotRepresentFloat1() : void - { - // The GraphQL specification does not allow serializing non-integer values - // as Int to avoid accidental data loss. - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non-integer value: 0.1'); - $intType->serialize(0.1); - } - - public function testSerializesOutputIntCannotRepresentFloat2() : void - { - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non-integer value: 1.1'); - $intType->serialize(1.1); - } - - public function testSerializesOutputIntCannotRepresentNegativeFloat() : void - { - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non-integer value: -1.1'); - $intType->serialize(-1.1); - } - - public function testSerializesOutputIntCannotRepresentNumericString() : void - { - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non 32-bit signed integer value: Int cannot represent non-integer value: "-1.1"'); - $intType->serialize('Int cannot represent non-integer value: "-1.1"'); - } - - public function testSerializesOutputIntCannotRepresentBiggerThan32Bits() : void - { - // Maybe a safe PHP int, but bigger than 2^32, so not - // representable as a GraphQL Int - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non 32-bit signed integer value: 9876504321'); - $intType->serialize(9876504321); - } - - public function testSerializesOutputIntCannotRepresentLowerThan32Bits() : void - { - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non 32-bit signed integer value: -9876504321'); - $intType->serialize(-9876504321); - } - - public function testSerializesOutputIntCannotRepresentBiggerThanSigned32Bits() : void - { - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non 32-bit signed integer value: 1.0E+100'); - $intType->serialize(1e100); - } - - public function testSerializesOutputIntCannotRepresentLowerThanSigned32Bits() : void + public function badIntValues() { - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non 32-bit signed integer value: -1.0E+100'); - $intType->serialize(-1e100); - } - - public function testSerializesOutputIntCannotRepresentString() : void - { - $intType = Type::int(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non 32-bit signed integer value: one'); - $intType->serialize('one'); + return [ + [0.1, 'Int cannot represent non-integer value: 0.1'], + [1.1, 'Int cannot represent non-integer value: 1.1'], + [-1.1, 'Int cannot represent non-integer value: -1.1'], + ['-1.1', 'Int cannot represent non-integer value: -1.1'], + [9876504321, 'Int cannot represent non 32-bit signed integer value: 9876504321'], + [-9876504321, 'Int cannot represent non 32-bit signed integer value: -9876504321'], + [1e100, 'Int cannot represent non 32-bit signed integer value: 1.0E+100'], + [-1e100, 'Int cannot represent non 32-bit signed integer value: -1.0E+100'], + [log(0), 'Int cannot represent non 32-bit signed integer value: -INF'], + [acos(8), 'Int cannot represent non-integer value: NAN'], + ['one', 'Int cannot represent non-integer value: one'], + ['', 'Int cannot represent non-integer value: (empty string)'], + [[5], 'Int cannot represent non-integer value: [5]'], + ]; } - public function testSerializesOutputIntCannotRepresentEmptyString() : void + /** + * @throws Error + * + * @dataProvider badIntValues + */ + public function testSerializesOutputAsIntErrors($value, $expectedError) : void { + // The GraphQL specification does not allow serializing non-integer values + // as Int to avoid accidental data loss. $intType = Type::int(); $this->expectException(Error::class); - $this->expectExceptionMessage('Int cannot represent non 32-bit signed integer value: (empty string)'); - $intType->serialize(''); + $this->expectExceptionMessage($expectedError); + $intType->serialize($value); } /** - * @see it('serializes output float') + * @see it('serializes output as Float') */ - public function testSerializesOutputFloat() : void + public function testSerializesOutputAsFloat() : void { $floatType = Type::float(); @@ -132,90 +87,120 @@ public function testSerializesOutputFloat() : void self::assertSame(1.0, $floatType->serialize(true)); } - public function testSerializesOutputFloatCannotRepresentString() : void + public function badFloatValues() { - $floatType = Type::float(); - $this->expectException(Error::class); - $this->expectExceptionMessage('Float cannot represent non numeric value: one'); - $floatType->serialize('one'); + return [ + ['one', 'Float cannot represent non numeric value: one'], + ['', 'Float cannot represent non numeric value: (empty string)'], + [log(0), 'Float cannot represent non numeric value: -INF'], + [acos(8), 'Float cannot represent non numeric value: NAN'], + [[5], 'Float cannot represent non numeric value: [5]'], + ]; } - public function testSerializesOutputFloatCannotRepresentEmptyString() : void + /** + * @throws Error + * + * @dataProvider badFloatValues + */ + public function testSerializesOutputFloatErrors($value, $expectedError) : void { $floatType = Type::float(); $this->expectException(Error::class); - $this->expectExceptionMessage('Float cannot represent non numeric value: (empty string)'); - $floatType->serialize(''); + $this->expectExceptionMessage($expectedError); + $floatType->serialize($value); } /** - * @see it('serializes output strings') + * @see it('serializes output as String') */ - public function testSerializesOutputStrings() : void + public function testSerializesOutputAsString() : void { $stringType = Type::string(); - self::assertSame('string', $stringType->serialize('string')); self::assertSame('1', $stringType->serialize(1)); self::assertSame('-1.1', $stringType->serialize(-1.1)); - self::assertSame('true', $stringType->serialize(true)); - self::assertSame('false', $stringType->serialize(false)); - self::assertSame('null', $stringType->serialize(null)); - self::assertSame('2', $stringType->serialize(new ObjectIdStub(2))); + self::assertSame('1', $stringType->serialize(true)); + self::assertSame('', $stringType->serialize(false)); + self::assertSame('', $stringType->serialize(null)); + self::assertSame('foo', $stringType->serialize(new CanCastToString('foo'))); } - public function testSerializesOutputStringsCannotRepresentArray() : void + public function badStringValues() { - $stringType = Type::string(); - $this->expectException(Error::class); - $this->expectExceptionMessage('String cannot represent non scalar value: []'); - $stringType->serialize([]); + return [ + [[1], 'String cannot represent value: [1]'], + [new stdClass(), 'String cannot represent value: instance of stdClass'], + ]; } - public function testSerializesOutputStringsCannotRepresentObject() : void + /** + * @throws Error + * + * @dataProvider badStringValues + */ + public function testSerializesOutputStringErrors($value, $expectedError) : void { $stringType = Type::string(); $this->expectException(Error::class); - $this->expectExceptionMessage('String cannot represent non scalar value: instance of stdClass'); - $stringType->serialize(new stdClass()); + $this->expectExceptionMessage($expectedError); + $stringType->serialize($value); } /** - * @see it('serializes output boolean') + * @see it('serializes output as Boolean') */ - public function testSerializesOutputBoolean() : void + public function testSerializesOutputAsBoolean() : void { $boolType = Type::boolean(); - self::assertTrue($boolType->serialize('string')); - self::assertFalse($boolType->serialize('')); - self::assertTrue($boolType->serialize('1')); - self::assertTrue($boolType->serialize(1)); - self::assertFalse($boolType->serialize(0)); self::assertTrue($boolType->serialize(true)); + self::assertTrue($boolType->serialize(1)); + self::assertTrue($boolType->serialize('1')); + self::assertTrue($boolType->serialize('string')); + self::assertFalse($boolType->serialize(false)); - // TODO: how should it behave on '0'? + self::assertFalse($boolType->serialize(0)); + self::assertFalse($boolType->serialize('0')); + self::assertFalse($boolType->serialize('')); } - public function testSerializesOutputID() : void + /** + * @see it('serializes output as ID') + */ + public function testSerializesOutputAsID() : void { $idType = Type::id(); self::assertSame('string', $idType->serialize('string')); + self::assertSame('false', $idType->serialize('false')); self::assertSame('', $idType->serialize('')); self::assertSame('1', $idType->serialize('1')); + self::assertSame('0', $idType->serialize('0')); self::assertSame('1', $idType->serialize(1)); self::assertSame('0', $idType->serialize(0)); - self::assertSame('true', $idType->serialize(true)); - self::assertSame('false', $idType->serialize(false)); self::assertSame('2', $idType->serialize(new ObjectIdStub(2))); } - public function testSerializesOutputIDCannotRepresentObject() : void + public function badIDValues() + { + return [ + [new stdClass(), 'ID cannot represent value: instance of stdClass'], + [true, 'ID cannot represent value: true'], + [false, 'ID cannot represent value: false'], + [-1.1, 'ID cannot represent value: -1.1'], + [['abc'], 'ID cannot represent value: ["abc"]'], + ]; + } + + /** + * @dataProvider badIDValues + */ + public function testSerializesOutputAsIDError($value, $expectedError) { $idType = Type::id(); $this->expectException(Error::class); - $this->expectExceptionMessage('ID type cannot represent non scalar value: instance of stdClass'); - $idType->serialize(new stdClass()); + $this->expectExceptionMessage($expectedError); + $idType->serialize($value); } } diff --git a/tests/Type/SchemaTest.php b/tests/Type/SchemaTest.php index ddb832b77..9b401a49f 100644 --- a/tests/Type/SchemaTest.php +++ b/tests/Type/SchemaTest.php @@ -33,7 +33,7 @@ class SchemaTest extends TestCase /** @var Schema */ private $schema; - public function setUp() + public function setUp() : void { $this->interfaceType = new InterfaceType([ 'name' => 'Interface', @@ -46,7 +46,7 @@ public function setUp() 'fields' => [ 'fieldName' => [ 'type' => Type::string(), - 'resolve' => static function () { + 'resolve' => static function () : string { return ''; }, ], @@ -90,7 +90,7 @@ public function setUp() 'fields' => [ 'getObject' => [ 'type' => $this->interfaceType, - 'resolve' => static function () { + 'resolve' => static function () : array { return []; }, ], diff --git a/tests/Type/StandardTypesTest.php b/tests/Type/StandardTypesTest.php index 93f3ceb67..1f1989142 100644 --- a/tests/Type/StandardTypesTest.php +++ b/tests/Type/StandardTypesTest.php @@ -15,12 +15,12 @@ class StandardTypesTest extends TestCase /** @var Type[] */ private static $originalStandardTypes; - public static function setUpBeforeClass() + public static function setUpBeforeClass() : void { self::$originalStandardTypes = Type::getStandardTypes(); } - public function tearDown() + public function tearDown() : void { parent::tearDown(); Type::overrideStandardTypes(self::$originalStandardTypes); @@ -121,11 +121,11 @@ private function createCustomScalarType($name) { return new CustomScalarType([ 'name' => $name, - 'serialize' => static function () { + 'serialize' => static function () : void { }, - 'parseValue' => static function () { + 'parseValue' => static function () : void { }, - 'parseLiteral' => static function () { + 'parseLiteral' => static function () : void { }, ]); } diff --git a/tests/Type/TestClasses/CanCastToString.php b/tests/Type/TestClasses/CanCastToString.php new file mode 100644 index 000000000..017b485f6 --- /dev/null +++ b/tests/Type/TestClasses/CanCastToString.php @@ -0,0 +1,21 @@ +str = $str; + } + + public function __toString() + { + return $this->str; + } +} diff --git a/tests/Type/ObjectIdStub.php b/tests/Type/TestClasses/ObjectIdStub.php similarity index 87% rename from tests/Type/ObjectIdStub.php rename to tests/Type/TestClasses/ObjectIdStub.php index 3049569f4..a6c2b64e7 100644 --- a/tests/Type/ObjectIdStub.php +++ b/tests/Type/TestClasses/ObjectIdStub.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace GraphQL\Tests\Type; +namespace GraphQL\Tests\Type\TestClasses; class ObjectIdStub { diff --git a/tests/Type/TypeLoaderTest.php b/tests/Type/TypeLoaderTest.php index e7193ec74..18ae90376 100644 --- a/tests/Type/TypeLoaderTest.php +++ b/tests/Type/TypeLoaderTest.php @@ -6,6 +6,7 @@ use Exception; use GraphQL\Error\InvariantViolation; +use GraphQL\Tests\PHPUnit\ArraySubsetAsserts; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; @@ -18,6 +19,8 @@ class TypeLoaderTest extends TestCase { + use ArraySubsetAsserts; + /** @var ObjectType */ private $query; @@ -45,26 +48,26 @@ class TypeLoaderTest extends TestCase /** @var string[] */ private $calls; - public function setUp() + public function setUp() : void { $this->calls = []; $this->node = new InterfaceType([ 'name' => 'Node', - 'fields' => function () { + 'fields' => function () : array { $this->calls[] = 'Node.fields'; return [ 'id' => Type::string(), ]; }, - 'resolveType' => static function () { + 'resolveType' => static function () : void { }, ]); $this->content = new InterfaceType([ 'name' => 'Content', - 'fields' => function () { + 'fields' => function () : array { $this->calls[] = 'Content.fields'; return [ @@ -72,7 +75,7 @@ public function setUp() 'body' => Type::string(), ]; }, - 'resolveType' => static function () { + 'resolveType' => static function () : void { }, ]); @@ -82,7 +85,7 @@ public function setUp() $this->node, $this->content, ], - 'fields' => function () { + 'fields' => function () : array { $this->calls[] = 'BlogStory.fields'; return [ @@ -95,7 +98,7 @@ public function setUp() $this->query = new ObjectType([ 'name' => 'Query', - 'fields' => function () { + 'fields' => function () : array { $this->calls[] = 'Query.fields'; return [ @@ -107,7 +110,7 @@ public function setUp() $this->mutation = new ObjectType([ 'name' => 'Mutation', - 'fields' => function () { + 'fields' => function () : array { $this->calls[] = 'Mutation.fields'; return [ @@ -155,7 +158,7 @@ public function testSchemaAcceptsTypeLoader() : void 'name' => 'Query', 'fields' => ['a' => Type::string()], ]), - 'typeLoader' => static function () { + 'typeLoader' => static function () : void { }, ]); } @@ -233,9 +236,21 @@ public function testWorksWithTypeLoader() : void self::assertSame($this->postStoryMutationInput, $input); self::assertEquals(['Node', 'Content', 'PostStoryMutationInput'], $this->calls); - $result = $schema->isPossibleType($this->node, $this->blogStory); + $result = $schema->isSubType($this->node, $this->blogStory); self::assertTrue($result); - self::assertEquals(['Node', 'Content', 'PostStoryMutationInput'], $this->calls); + self::assertEquals( + [ + 'Node', + 'Content', + 'PostStoryMutationInput', + 'Query.fields', + 'Content.fields', + 'Node.fields', + 'Mutation.fields', + 'BlogStory.fields', + ], + $this->calls + ); } public function testOnlyCallsLoaderOnce() : void @@ -256,12 +271,12 @@ public function testFailsOnNonExistentType() : void { $schema = new Schema([ 'query' => $this->query, - 'typeLoader' => static function () { + 'typeLoader' => static function () : void { }, ]); $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage('Type loader is expected to return valid type "NonExistingType", but it returned null'); + $this->expectExceptionMessage('Type loader is expected to return a callable or valid type "NonExistingType", but it returned null'); $schema->getType('NonExistingType'); } @@ -270,13 +285,13 @@ public function testFailsOnNonType() : void { $schema = new Schema([ 'query' => $this->query, - 'typeLoader' => static function () { + 'typeLoader' => static function () : stdClass { return new stdClass(); }, ]); $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage('Type loader is expected to return valid type "Node", but it returned instance of stdClass'); + $this->expectExceptionMessage('Type loader is expected to return a callable or valid type "Node", but it returned instance of stdClass'); $schema->getType('Node'); } @@ -285,7 +300,7 @@ public function testFailsOnInvalidLoad() : void { $schema = new Schema([ 'query' => $this->query, - 'typeLoader' => function () { + 'typeLoader' => function () : InterfaceType { return $this->content; }, ]); @@ -300,7 +315,7 @@ public function testPassesThroughAnExceptionInLoader() : void { $schema = new Schema([ 'query' => $this->query, - 'typeLoader' => static function () { + 'typeLoader' => static function () : void { throw new Exception('This is the exception we are looking for'); }, ]); diff --git a/tests/Type/ValidationTest.php b/tests/Type/ValidationTest.php index 5bafd30ec..4290c4526 100644 --- a/tests/Type/ValidationTest.php +++ b/tests/Type/ValidationTest.php @@ -7,24 +7,24 @@ use GraphQL\Error\Error; use GraphQL\Error\InvariantViolation; use GraphQL\Error\Warning; +use GraphQL\Language\Parser; use GraphQL\Language\SourceLocation; use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; +use GraphQL\Type\Definition\ListOfType; +use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Schema; use GraphQL\Utils\BuildSchema; +use GraphQL\Utils\SchemaExtender; use GraphQL\Utils\Utils; use PHPUnit\Framework\TestCase; -use function array_map; use function array_merge; -use function implode; -use function print_r; -use function sprintf; class ValidationTest extends TestCase { @@ -61,33 +61,33 @@ class ValidationTest extends TestCase /** @var float */ public $Number; - public function setUp() + public function setUp() : void { $this->Number = 1; $this->SomeScalarType = new CustomScalarType([ 'name' => 'SomeScalar', - 'serialize' => static function () { + 'serialize' => static function () : void { }, - 'parseValue' => static function () { + 'parseValue' => static function () : void { }, - 'parseLiteral' => static function () { + 'parseLiteral' => static function () : void { }, ]); $this->SomeInterfaceType = new InterfaceType([ 'name' => 'SomeInterface', - 'fields' => function () { + 'fields' => function () : array { return ['f' => ['type' => $this->SomeObjectType]]; }, ]); $this->SomeObjectType = new ObjectType([ 'name' => 'SomeObject', - 'fields' => function () { + 'fields' => function () : array { return ['f' => ['type' => $this->SomeObjectType]]; }, - 'interfaces' => function () { + 'interfaces' => function () : array { return [$this->SomeInterfaceType]; }, ]); @@ -146,26 +146,26 @@ private function withModifiers($types) $types, Utils::map( $types, - static function ($type) { + static function ($type) : ListOfType { return Type::listOf($type); } ), Utils::map( $types, - static function ($type) { + static function ($type) : NonNull { return Type::nonNull($type); } ), Utils::map( $types, - static function ($type) { + static function ($type) : NonNull { return Type::nonNull(Type::listOf($type)); } ) ); } - public function tearDown() + public function tearDown() : void { parent::tearDown(); Warning::enable(Warning::WARNING_NOT_A_TYPE); @@ -175,19 +175,19 @@ public function testRejectsTypesWithoutNames() : void { $this->assertEachCallableThrows( [ - static function () { + static function () : ObjectType { return new ObjectType([]); }, - static function () { + static function () : EnumType { return new EnumType([]); }, - static function () { + static function () : InputObjectType { return new InputObjectType([]); }, - static function () { + static function () : UnionType { return new UnionType([]); }, - static function () { + static function () : InterfaceType { return new InterfaceType([]); }, ], @@ -337,7 +337,7 @@ public function testRejectsASchemaWithoutAQueryType() : void private function formatLocations(Error $error) { - return Utils::map($error->getLocations(), static function (SourceLocation $loc) { + return Utils::map($error->getLocations(), static function (SourceLocation $loc) : array { return ['line' => $loc->line, 'column' => $loc->column]; }); } @@ -350,7 +350,7 @@ private function formatLocations(Error $error) */ private function formatErrors(array $errors, $withLocation = true) { - return Utils::map($errors, function (Error $error) use ($withLocation) { + return Utils::map($errors, function (Error $error) use ($withLocation) : array { if (! $withLocation) { return [ 'message' => $error->getMessage() ]; } @@ -516,6 +516,62 @@ public function testRejectsASchemaWhoseSubscriptionTypeIsAnInputType() : void ); } + /** + * @see it('rejects a schema extended with invalid root types') + */ + public function testRejectsASchemaExtendedWithInvalidRootTypes() + { + $schema = BuildSchema::build(' + input SomeInputObject { + test: String + } + '); + + $schema = SchemaExtender::extend( + $schema, + Parser::parse(' + extend schema { + query: SomeInputObject + } + ') + ); + + $schema = SchemaExtender::extend( + $schema, + Parser::parse(' + extend schema { + mutation: SomeInputObject + } + ') + ); + + $schema = SchemaExtender::extend( + $schema, + Parser::parse(' + extend schema { + subscription: SomeInputObject + } + ') + ); + + $expected = [ + [ + 'message' => 'Query root type must be Object type, it cannot be SomeInputObject.', + 'locations' => [[ 'line' => 2, 'column' => 13 ]], + ], + [ + 'message' => 'Mutation root type must be Object type if provided, it cannot be SomeInputObject.', + 'locations' => [[ 'line' => 2, 'column' => 13 ]], + ], + [ + 'message' => 'Subscription root type must be Object type if provided, it cannot be SomeInputObject.', + 'locations' => [[ 'line' => 2, 'column' => 13 ]], + ], + ]; + + $this->assertMatchesValidationMessage($schema->validate(), $expected); + } + /** * @see it('rejects a Schema whose directives are incorrectly typed') */ @@ -587,7 +643,7 @@ public function testRejectsAnObjectTypeWithMissingFields() : void $manualSchema2 = $this->schemaWithFieldType( new ObjectType([ 'name' => 'IncompleteObject', - 'fields' => static function () { + 'fields' => static function () : array { return []; }, ]) @@ -731,17 +787,27 @@ public function testAcceptsAUnionTypeWithArrayTypes() : void public function testRejectsAUnionTypeWithEmptyTypes() : void { $schema = BuildSchema::build(' - type Query { - test: BadUnion - } - - union BadUnion + type Query { + test: BadUnion + } + + union BadUnion '); + + $schema = SchemaExtender::extend( + $schema, + Parser::parse(' + directive @test on UNION + + extend union BadUnion @test + ') + ); + $this->assertMatchesValidationMessage( $schema->validate(), [[ 'message' => 'Union type BadUnion must define one or more member types.', - 'locations' => [['line' => 6, 'column' => 7]], + 'locations' => [['line' => 6, 'column' => 13], ['line' => 4, 'column' => 11]], ], ] ); @@ -778,6 +844,25 @@ public function testRejectsAUnionTypeWithDuplicatedMemberType() : void ], ] ); + + $extendedSchema = SchemaExtender::extend( + $schema, + Parser::parse('extend union BadUnion = TypeB') + ); + + $this->assertMatchesValidationMessage( + $extendedSchema->validate(), + [ + [ + 'message' => 'Union type BadUnion can only include type TypeA once.', + 'locations' => [['line' => 15, 'column' => 11], ['line' => 17, 'column' => 11]], + ], + [ + 'message' => 'Union type BadUnion can only include type TypeB once.', + 'locations' => [[ 'line' => 16, 'column' => 11 ], [ 'line' => 3, 'column' => 5 ]], + ], + ] + ); } /** @@ -803,13 +888,23 @@ public function testRejectsAUnionTypeWithNonObjectMembersType() : void | String | TypeB '); + + $schema = SchemaExtender::extend( + $schema, + Parser::parse('extend union BadUnion = Int') + ); + $this->assertMatchesValidationMessage( $schema->validate(), - [[ - 'message' => 'Union type BadUnion can only include Object types, ' . - 'it cannot include String.', - 'locations' => [['line' => 16, 'column' => 11]], - ], + [ + [ + 'message' => 'Union type BadUnion can only include Object types, it cannot include String.', + 'locations' => [['line' => 16, 'column' => 11]], + ], + [ + 'message' => 'Union type BadUnion can only include Object types, it cannot include Int.', + 'locations' => [[ 'line' => 1, 'column' => 25 ]], + ], ] ); @@ -869,12 +964,161 @@ public function testRejectsAnInputObjectTypeWithMissingFields() : void input SomeInputObject '); + + $schema = SchemaExtender::extend( + $schema, + Parser::parse(' + directive @test on INPUT_OBJECT + + extend input SomeInputObject @test + ') + ); + $this->assertMatchesValidationMessage( $schema->validate(), - [[ - 'message' => 'Input Object type SomeInputObject must define one or more fields.', - 'locations' => [['line' => 6, 'column' => 7]], - ], + [ + [ + 'message' => 'Input Object type SomeInputObject must define one or more fields.', + 'locations' => [['line' => 6, 'column' => 7], ['line' => 3, 'column' => 31]], + ], + ] + ); + } + + /** + * @see it('accepts an Input Object with breakable circular reference') + */ + public function testAcceptsAnInputObjectWithBreakableCircularReference() : void + { + $schema = BuildSchema::build(' + input AnotherInputObject { + parent: SomeInputObject + } + + type Query { + field(arg: SomeInputObject): String + } + + input SomeInputObject { + self: SomeInputObject + arrayOfSelf: [SomeInputObject] + nonNullArrayOfSelf: [SomeInputObject]! + nonNullArrayOfNonNullSelf: [SomeInputObject!]! + intermediateSelf: AnotherInputObject + } + '); + self::assertEquals([], $schema->validate()); + } + + /** + * @see it('rejects an Input Object with non-breakable circular reference') + */ + public function testRejectsAnInputObjectWithNonBreakableCircularReference() : void + { + $schema = BuildSchema::build(' + type Query { + field(arg: SomeInputObject): String + } + + input SomeInputObject { + nonNullSelf: SomeInputObject! + } + '); + $this->assertMatchesValidationMessage( + $schema->validate(), + [ + [ + 'message' => 'Cannot reference Input Object "SomeInputObject" within itself through a series of non-null fields: "nonNullSelf".', + 'locations' => [['line' => 7, 'column' => 9]], + ], + ] + ); + } + + /** + * @see it('rejects Input Objects with non-breakable circular reference spread across them') + */ + public function testRejectsInputObjectsWithNonBreakableCircularReferenceSpreadAcrossThem() : void + { + $schema = BuildSchema::build(' + type Query { + field(arg: SomeInputObject): String + } + + input SomeInputObject { + startLoop: AnotherInputObject! + } + + input AnotherInputObject { + nextInLoop: YetAnotherInputObject! + } + + input YetAnotherInputObject { + closeLoop: SomeInputObject! + } + '); + $this->assertMatchesValidationMessage( + $schema->validate(), + [ + [ + 'message' => 'Cannot reference Input Object "SomeInputObject" within itself through a series of non-null fields: "startLoop.nextInLoop.closeLoop".', + 'locations' => [ + ['line' => 7, 'column' => 9], + ['line' => 11, 'column' => 9], + ['line' => 15, 'column' => 9], + ], + ], + ] + ); + } + + /** + * @see it('rejects Input Objects with multiple non-breakable circular reference') + */ + public function testRejectsInputObjectsWithMultipleNonBreakableCircularReferences() : void + { + $schema = BuildSchema::build(' + type Query { + field(arg: SomeInputObject): String + } + + input SomeInputObject { + startLoop: AnotherInputObject! + } + + input AnotherInputObject { + closeLoop: SomeInputObject! + startSecondLoop: YetAnotherInputObject! + } + + input YetAnotherInputObject { + closeSecondLoop: AnotherInputObject! + nonNullSelf: YetAnotherInputObject! + } + '); + $this->assertMatchesValidationMessage( + $schema->validate(), + [ + [ + 'message' => 'Cannot reference Input Object "SomeInputObject" within itself through a series of non-null fields: "startLoop.closeLoop".', + 'locations' => [ + ['line' => 7, 'column' => 9], + ['line' => 11, 'column' => 9], + ], + ], + [ + 'message' => 'Cannot reference Input Object "AnotherInputObject" within itself through a series of non-null fields: "startSecondLoop.closeSecondLoop".', + 'locations' => [ + ['line' => 12, 'column' => 9], + ['line' => 16, 'column' => 9], + ], + ], + [ + 'message' => 'Cannot reference Input Object "YetAnotherInputObject" within itself through a series of non-null fields: "nonNullSelf".', + 'locations' => [ + ['line' => 17, 'column' => 9], + ], + ], ] ); } @@ -928,11 +1172,21 @@ public function testRejectsAnEnumTypeWithoutValues() : void enum SomeEnum '); + + $schema = SchemaExtender::extend( + $schema, + Parser::parse(' + directive @test on ENUM + + extend enum SomeEnum @test + ') + ); + $this->assertMatchesValidationMessage( $schema->validate(), [[ 'message' => 'Enum type SomeEnum must define one or more values.', - 'locations' => [['line' => 6, 'column' => 7]], + 'locations' => [['line' => 6, 'column' => 7], ['line' => 3, 'column' => 23]], ], ] ); @@ -1055,20 +1309,6 @@ private function schemaWithObjectFieldOfType($fieldType) : Schema ]); } - /** - * @see it('rejects an empty Object field type') - */ - public function testRejectsAnEmptyObjectFieldType() : void - { - $schema = $this->schemaWithObjectFieldOfType(null); - - $this->assertMatchesValidationMessage( - $schema->validate(), - [['message' => 'The type of BadObject.badField must be Output Type but got: null.'], - ] - ); - } - /** * @see it('rejects a non-output type as an Object field type') */ @@ -1087,21 +1327,6 @@ public function testRejectsANonOutputTypeAsAnObjectFieldType() : void } } - /** - * @see it('rejects a non-type value as an Object field type') - */ - public function testRejectsANonTypeValueAsAnObjectFieldType() - { - $schema = $this->schemaWithObjectFieldOfType($this->Number); - $this->assertMatchesValidationMessage( - $schema->validate(), - [ - ['message' => 'The type of BadObject.badField must be Output Type but got: 1.'], - ['message' => 'Expected GraphQL named type but got: 1.'], - ] - ); - } - /** * @see it('rejects with relevant locations for a non-output type as an Object field type') */ @@ -1126,28 +1351,6 @@ public function testRejectsWithReleventLocationsForANonOutputTypeAsAnObjectField ); } - // DESCRIBE: Type System: Interface fields must have output types - - /** - * @see it('rejects an Object implementing a non-type values') - */ - public function testRejectsAnObjectImplementingANonTypeValues() : void - { - $schema = new Schema([ - 'query' => new ObjectType([ - 'name' => 'BadObject', - 'interfaces' => [null], - 'fields' => ['a' => Type::string()], - ]), - ]); - $expected = ['message' => 'Type BadObject must only implement Interface types, it cannot implement null.']; - - $this->assertMatchesValidationMessage( - $schema->validate(), - [$expected] - ); - } - /** * @see it('rejects an Object implementing a non-Interface type') */ @@ -1236,6 +1439,151 @@ interface AnotherInterface { ); } + // DESCRIBE: Type System: Interface extensions should be valid + + /** + * @see it('rejects an Object implementing the extended interface due to missing field') + */ + public function testRejectsAnObjectImplementingTheExtendedInterfaceDueToMissingField() + { + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } + + interface AnotherInterface { + field: String + } + + type AnotherObject implements AnotherInterface { + field: String + }'); + + $extendedSchema = SchemaExtender::extend( + $schema, + Parser::parse(' + extend interface AnotherInterface { + newField: String + } + + extend type AnotherObject { + differentNewField: String + } + ') + ); + + $this->assertMatchesValidationMessage( + $extendedSchema->validate(), + [[ + 'message' => 'Interface field AnotherInterface.newField expected but AnotherObject does not provide it.', + 'locations' => [ + ['line' => 3, 'column' => 19], + ['line' => 7, 'column' => 7], + ['line' => 6, 'column' => 17], + ], + ], + ] + ); + } + + /** + * @see it('rejects an Object implementing the extended interface due to missing field args') + */ + public function testRejectsAnObjectImplementingTheExtendedInterfaceDueToMissingFieldArgs() + { + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } + + interface AnotherInterface { + field: String + } + + type AnotherObject implements AnotherInterface { + field: String + }'); + + $extendedSchema = SchemaExtender::extend( + $schema, + Parser::parse(' + extend interface AnotherInterface { + newField(test: Boolean): String + } + + extend type AnotherObject { + newField: String + } + ') + ); + + $this->assertMatchesValidationMessage( + $extendedSchema->validate(), + [[ + 'message' => 'Interface field argument AnotherInterface.newField(test:) expected but AnotherObject.newField does not provide it.', + 'locations' => [ + ['line' => 3, 'column' => 28], + ['line' => 7, 'column' => 19], + ], + ], + ] + ); + } + + /** + * @see it('rejects Objects implementing the extended interface due to mismatching interface type') + */ + public function testRejectsObjectsImplementingTheExtendedInterfaceDueToMismatchingInterfaceType() + { + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } + + interface AnotherInterface { + field: String + } + + type AnotherObject implements AnotherInterface { + field: String + }'); + + $extendedSchema = SchemaExtender::extend( + $schema, + Parser::parse(' + extend interface AnotherInterface { + newInterfaceField: NewInterface + } + + interface NewInterface { + newField: String + } + + interface MismatchingInterface { + newField: String + } + + extend type AnotherObject { + newInterfaceField: MismatchingInterface + } + + # Required to prevent unused interface errors + type DummyObject implements NewInterface & MismatchingInterface { + newField: String + } + ') + ); + + $this->assertMatchesValidationMessage( + $extendedSchema->validate(), + [[ + 'message' => 'Interface field AnotherInterface.newInterfaceField expects type NewInterface but AnotherObject.newInterfaceField is type MismatchingInterface.', + 'locations' => [['line' => 3, 'column' => 38], ['line' => 15, 'column' => 38]], + ], + ] + ); + } + // DESCRIBE: Type System: Field arguments must have input types /** @@ -1278,24 +1626,9 @@ private function schemaWithInterfaceFieldOfType($fieldType) } /** - * @see it('rejects an empty Interface field type') + * @see it('rejects a non-output type as an Interface field type') */ - public function testRejectsAnEmptyInterfaceFieldType() : void - { - $schema = $this->schemaWithInterfaceFieldOfType(null); - $this->assertMatchesValidationMessage( - $schema->validate(), - [ - ['message' => 'The type of BadInterface.badField must be Output Type but got: null.'], - ['message' => 'The type of BadImplementing.badField must be Output Type but got: null.'], - ] - ); - } - - /** - * @see it('rejects a non-output type as an Interface field type') - */ - public function testRejectsANonOutputTypeAsAnInterfaceFieldType() : void + public function testRejectsANonOutputTypeAsAnInterfaceFieldType() : void { foreach ($this->notOutputTypes as $type) { $schema = $this->schemaWithInterfaceFieldOfType($type); @@ -1310,22 +1643,6 @@ public function testRejectsANonOutputTypeAsAnInterfaceFieldType() : void } } - /** - * @see it('rejects a non-type value as an Interface field type') - */ - public function testRejectsANonTypeValueAsAnInterfaceFieldType() - { - $schema = $this->schemaWithInterfaceFieldOfType('string'); - $this->assertMatchesValidationMessage( - $schema->validate(), - [ - ['message' => 'The type of BadInterface.badField must be Output Type but got: string.'], - ['message' => 'Expected GraphQL named type but got: string.'], - ['message' => 'The type of BadImplementing.badField must be Output Type but got: string.'], - ] - ); - } - // DESCRIBE: Type System: Input Object fields must have input types /** @@ -1366,7 +1683,7 @@ interface SomeInterface { } /** - * @see it('rejects an interface not implemented by at least one object') + * @see it('accepts an interface not implemented by at least one object') */ public function testRejectsAnInterfaceNotImplementedByAtLeastOneObject() { @@ -1381,11 +1698,7 @@ interface SomeInterface { '); $this->assertMatchesValidationMessage( $schema->validate(), - [[ - 'message' => 'Interface SomeInterface must be implemented by at least one Object type.', - 'locations' => [[ 'line' => 6, 'column' => 7 ]], - ], - ] + [] ); } @@ -1425,19 +1738,6 @@ private function schemaWithArgOfType($argType) ]); } - /** - * @see it('rejects an empty field arg type') - */ - public function testRejectsAnEmptyFieldArgType() : void - { - $schema = $this->schemaWithArgOfType(null); - $this->assertMatchesValidationMessage( - $schema->validate(), - [['message' => 'The type of BadObject.badField(badArg:) must be Input Type but got: null.'], - ] - ); - } - // DESCRIBE: Objects must adhere to Interface they implement /** @@ -1456,21 +1756,6 @@ public function testRejectsANonInputTypeAsAFieldArgType() : void } } - /** - * @see it('rejects a non-type value as a field arg type') - */ - public function testRejectsANonTypeValueAsAFieldArgType() - { - $schema = $this->schemaWithArgOfType('string'); - $this->assertMatchesValidationMessage( - $schema->validate(), - [ - ['message' => 'The type of BadObject.badField(badArg:) must be Input Type but got: string.'], - ['message' => 'Expected GraphQL named type but got: string.'], - ] - ); - } - /** * @see it('rejects a non-input type as a field arg with locations') */ @@ -1508,7 +1793,7 @@ public function testAcceptsAnInputTypeAsAnInputFieldType() : void private function schemaWithInputFieldOfType($inputFieldType) { - $BadInputObjectType = new InputObjectType([ + $badInputObjectType = new InputObjectType([ 'name' => 'BadInputObject', 'fields' => [ 'badField' => ['type' => $inputFieldType], @@ -1522,7 +1807,7 @@ private function schemaWithInputFieldOfType($inputFieldType) 'f' => [ 'type' => Type::string(), 'args' => [ - 'badArg' => ['type' => $BadInputObjectType], + 'badArg' => ['type' => $badInputObjectType], ], ], ], @@ -1531,19 +1816,6 @@ private function schemaWithInputFieldOfType($inputFieldType) ]); } - /** - * @see it('rejects an empty input field type') - */ - public function testRejectsAnEmptyInputFieldType() : void - { - $schema = $this->schemaWithInputFieldOfType(null); - $this->assertMatchesValidationMessage( - $schema->validate(), - [['message' => 'The type of BadInputObject.badField must be Input Type but got: null.'], - ] - ); - } - /** * @see it('rejects a non-input type as an input field type') */ @@ -1561,21 +1833,6 @@ public function testRejectsANonInputTypeAsAnInputFieldType() : void } } - /** - * @see it('rejects a non-type value as an input field type') - */ - public function testRejectsAAonTypeValueAsAnInputFieldType() - { - $schema = $this->schemaWithInputFieldOfType('string'); - $this->assertMatchesValidationMessage( - $schema->validate(), - [ - ['message' => 'The type of BadInputObject.badField must be Input Type but got: string.'], - ['message' => 'Expected GraphQL named type but got: string.'], - ] - ); - } - /** * @see it('rejects a non-input type as an input object field with locations') */ @@ -1930,21 +2187,27 @@ public function testRejectsAnObjectWhichImplementsAnInterfaceFieldAlongWithAddit } interface AnotherInterface { - field(input: String): String + field(baseArg: String): String } type AnotherObject implements AnotherInterface { - field(input: String, anotherInput: String!): String + field( + baseArg: String, + requiredArg: String! + optionalArg1: String, + optionalArg2: String = "", + ): String } '); $this->assertMatchesValidationMessage( $schema->validate(), [[ - 'message' => 'Object field argument AnotherObject.field(anotherInput:) is of ' . - 'required type String! but is not also provided by the Interface ' . - 'field AnotherInterface.field.', - 'locations' => [['line' => 11, 'column' => 44], ['line' => 7, 'column' => 9]], + 'message' => + 'Object field AnotherObject.field includes required argument ' . + 'requiredArg that is missing from the Interface field ' . + 'AnotherInterface.field.', + 'locations' => [['line' => 13, 'column' => 11], ['line' => 7, 'column' => 9]], ], ] ); @@ -2084,32 +2347,915 @@ interface AnotherInterface { ); } - public function testRejectsDifferentInstancesOfTheSameType() : void + /** + * @see it('rejects an Object missing a transitive interface') + */ + public function testRejectsAnObjectMissingATransitiveInterface() : void { - // Invalid: always creates new instance vs returning one from registry - $typeLoader = static function ($name) { - switch ($name) { - case 'Query': - return new ObjectType([ - 'name' => 'Query', - 'fields' => [ - 'test' => Type::string(), - ], - ]); - default: - return null; - } - }; + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } + + interface SuperInterface { + field: String! + } + + interface AnotherInterface implements SuperInterface { + field: String! + } + + type AnotherObject implements AnotherInterface { + field: String! + } + '); - $schema = new Schema([ - 'query' => $typeLoader('Query'), - 'typeLoader' => $typeLoader, - ]); - $this->expectException(InvariantViolation::class); - $this->expectExceptionMessage( - 'Type loader returns different instance for Query than field/argument definitions. ' . - 'Make sure you always return the same instance for the same type name.' + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Type AnotherObject must implement SuperInterface ' . + 'because it is implemented by AnotherInterface.', + 'locations' => [['line' => 10, 'column' => 45], ['line' => 14, 'column' => 37]], + ], + ] + ); + } + + /** + * @see it('accepts an Interface which implements an Interface') + */ + public function testAcceptsAnInterfaceWhichImplementsAnInterface() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: String): String + } + '); + + self::assertEquals([], $schema->validate()); + } + + /** + * @see it('accepts an Interface which implements an Interface along with more fields') + */ + public function testAcceptsAnInterfaceWhichImplementsAnInterfaceAlongWithMoreFields() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: String): String + anotherField: String + } + '); + + self::assertEquals([], $schema->validate()); + } + + /** + * @see it('accepts an Interface which implements an Interface field along with additional optional arguments') + */ + public function testAcceptsAnInterfaceWhichImplementsAnInterfaceFieldAlongWithAdditionalOptionalArguments() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: String, anotherInput: String): String + } + '); + + self::assertEquals([], $schema->validate()); + } + + /** + * @see it('rejects an Interface missing an Interface field') + */ + public function testRejectsAnInterfaceMissingAnInterfaceField() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + anotherField: String + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Interface field ParentInterface.field expected ' . + 'but ChildInterface does not provide it.', + 'locations' => [['line' => 7, 'column' => 9], ['line' => 10, 'column' => 7]], + ], + ] + ); + } + + /** + * @see it('rejects an Interface with an incorrectly typed Interface field') + */ + public function testRejectsAnInterfaceWithAnIncorrectlyTypedInterfaceField() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: String): Int + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Interface field ParentInterface.field expects type String ' . + 'but ChildInterface.field is type Int.', + 'locations' => [['line' => 7, 'column' => 31], ['line' => 11, 'column' => 31]], + ], + ] + ); + } + + /** + * @see it('rejects an Interface with a differently typed Interface field') + */ + public function testRejectsAnInterfaceWithADifferentlyTypedInterfaceField() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + type A { foo: String } + type B { foo: String } + + interface ParentInterface { + field: A + } + + interface ChildInterface implements ParentInterface { + field: B + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Interface field ParentInterface.field expects type A ' . + 'but ChildInterface.field is type B.', + 'locations' => [['line' => 10, 'column' => 16], ['line' => 14, 'column' => 16]], + ], + ] + ); + } + + /** + * @see it('accepts an interface with a subtyped Interface field (interface)') + */ + public function testAcceptsAnInterfaceWithASubtypedInterfaceFieldInterface() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: ParentInterface + } + + interface ChildInterface implements ParentInterface { + field: ChildInterface + } + '); + + self::assertEquals([], $schema->validate()); + } + + /** + * @see it('accepts an interface with a subtyped Interface field (union)') + */ + public function testAcceptsAnInterfaceWithASubtypedInterfaceFieldUnion() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + type SomeObject { + field: String + } + union SomeUnionType = SomeObject + + interface ParentInterface { + field: SomeUnionType + } + + interface ChildInterface implements ParentInterface { + field: SomeObject + } + '); + + self::assertEquals([], $schema->validate()); + } + + /** + * @see it('rejects an Interface with an Interface argument') + */ + public function testRejectsAnInterfaceMissingAnInterfaceArgument() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field: String + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Interface field argument ParentInterface.field(input:) expected ' . + 'but ChildInterface.field does not provide it.', + 'locations' => [['line' => 7, 'column' => 15], ['line' => 11, 'column' => 9]], + ], + ] + ); + } + + /** + * @see it('rejects an Interface with an incorrectly typed Interface argument') + */ + public function testRejectsAnInterfaceWithAnIncorrectlyTypedInterfaceArgument() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: Int): String + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Interface field argument ParentInterface.field(input:) expects type String ' . + 'but ChildInterface.field(input:) is type Int.', + 'locations' => [['line' => 7, 'column' => 22], ['line' => 11, 'column' => 22]], + ], + ] + ); + } + + /** + * @see it('rejects an Interface with both an incorrectly typed field and argument') + */ + public function testRejectsAnInterfaceWithBothAnIncorrectlyTypedFieldAndArgument() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: Int): Int + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [ + [ + 'message' => 'Interface field ParentInterface.field expects type String ' . + 'but ChildInterface.field is type Int.', + 'locations' => [['line' => 7, 'column' => 31], ['line' => 11, 'column' => 28]], + ], + [ + 'message' => 'Interface field argument ParentInterface.field(input:) expects type String ' . + 'but ChildInterface.field(input:) is type Int.', + 'locations' => [['line' => 7, 'column' => 22], ['line' => 11, 'column' => 22]], + ], + ] + ); + } + + /** + * @see it('rejects an Interface which implements an Interface field along with additional required arguments') + */ + public function testRejectsAnInterfaceWhichImplementsAnInterfaceFieldAlongWithAdditionalRequiredArguments() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(baseArg: String): String + } + + interface ChildInterface implements ParentInterface { + field( + baseArg: String, + requiredArg: String! + optionalArg1: String, + optionalArg2: String = "", + ): String + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Object field ChildInterface.field includes required argument requiredArg ' . + 'that is missing from the Interface field ParentInterface.field.', + 'locations' => [['line' => 13, 'column' => 11], ['line' => 7, 'column' => 9]], + ], + ] + ); + } + + /** + * @see it('accepts an Interface with an equivalently wrapped Interface field type') + */ + public function testAcceptsAnInterfaceWithAnEquivalentlyWrappedInterfaceFieldType() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: [String]! + } + + interface ChildInterface implements ParentInterface { + field: [String]! + } + '); + + self::assertEquals([], $schema->validate()); + } + + /** + * @see it('rejects an Interface with a non-list Interface field list type') + */ + public function testRejectsAnInterfaceWithANonListInterfaceFieldListType() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: [String] + } + + interface ChildInterface implements ParentInterface { + field: String + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Interface field ParentInterface.field expects type [String] ' . + 'but ChildInterface.field is type String.', + 'locations' => [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]], + ], + ] + ); + } + + /** + * @see it('rejects an Interface with a list Interface field non-list type') + */ + public function testRejectsAnInterfaceWithAListInterfaceFieldNonListType() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: String + } + + interface ChildInterface implements ParentInterface { + field: [String] + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Interface field ParentInterface.field expects type String ' . + 'but ChildInterface.field is type [String].', + 'locations' => [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]], + ], + ] + ); + } + + /** + * @see it('accepts an Interface with a subset non-null Interface field type') + */ + public function testAcceptsAnInterfaceWithASubsetNonNullInterfaceFieldType() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: String + } + + interface ChildInterface implements ParentInterface { + field: String! + } + '); + + self::assertEquals([], $schema->validate()); + } + + /** + * @see it('rejects an Interface with a superset nullable interface field type') + */ + public function testRejectsAnInterfaceWithASupsersetNullableInterfaceFieldType() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: String! + } + + interface ChildInterface implements ParentInterface { + field: String + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Interface field ParentInterface.field expects type String! ' . + 'but ChildInterface.field is type String.', + 'locations' => [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]], + ], + ] + ); + } + + /** + * @see it('rejects an Object missing a transitive interface') + */ + public function testRejectsAnInterfaceMissingATransitiveInterface() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface SuperInterface { + field: String! + } + + interface ParentInterface implements SuperInterface { + field: String! + } + + interface ChildInterface implements ParentInterface { + field: String! + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Type ChildInterface must implement SuperInterface ' . + 'because it is implemented by ParentInterface.', + 'locations' => [['line' => 10, 'column' => 44], ['line' => 14, 'column' => 43]], + ], + ] + ); + } + + /** + * @see it('rejects a self reference interface') + */ + public function testRejectsASelfReferenceInterface() : void + { + $schema = BuildSchema::build(' + type Query { + test: FooInterface + } + + interface FooInterface implements FooInterface { + field: String! + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Type FooInterface cannot implement itself ' . + 'because it would create a circular reference.', + 'locations' => [['line' => 6, 'column' => 41]], + ], + ] + ); + } + + /** + * @see it('rejects a circulare Interface implementation') + */ + public function testRejectsACircularInterfaceImplementation() : void + { + $schema = BuildSchema::build(' + type Query { + test: FooInterface + } + + interface FooInterface implements BarInterface { + field: String! + } + + interface BarInterface implements FooInterface { + field: String! + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [ + [ + 'message' => 'Type FooInterface cannot implement BarInterface ' . + 'because it would create a circular reference.', + 'locations' => [['line' => 10, 'column' => 41], ['line' => 6, 'column' => 41]], + ], + [ + 'message' => 'Type BarInterface cannot implement FooInterface ' . + 'because it would create a circular reference.', + 'locations' => [['line' => 6, 'column' => 41], ['line' => 10, 'column' => 41]], + ], + ] + ); + } + + public function testRejectsDifferentInstancesOfTheSameType() : void + { + // Invalid: always creates new instance vs returning one from registry + $typeLoader = static function ($name) : ?ObjectType { + switch ($name) { + case 'Query': + return new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'test' => Type::string(), + ], + ]); + default: + return null; + } + }; + + $schema = new Schema([ + 'query' => $typeLoader('Query'), + 'typeLoader' => $typeLoader, + ]); + $this->expectException(InvariantViolation::class); + $this->expectExceptionMessage( + 'Type loader returns different instance for Query than field/argument definitions. ' . + 'Make sure you always return the same instance for the same type name.' + ); + $schema->assertValid(); + } + + // DESCRIBE: Type System: Schema directives must validate + + /** + * @see it('accepts a Schema with valid directives') + */ + public function testAcceptsASchemaWithValidDirectives() + { + $schema = BuildSchema::build(' + schema @testA @testB { + query: Query + } + + type Query @testA @testB { + test: AnInterface @testC + } + + directive @testA on SCHEMA | OBJECT | INTERFACE | UNION | SCALAR | INPUT_OBJECT | ENUM + directive @testB on SCHEMA | OBJECT | INTERFACE | UNION | SCALAR | INPUT_OBJECT | ENUM + directive @testC on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION + directive @testD on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION + + interface AnInterface @testA { + field: String! @testC + } + + type TypeA implements AnInterface @testA { + field(arg: SomeInput @testC): String! @testC @testD + } + + type TypeB @testB @testA { + scalar_field: SomeScalar @testC + enum_field: SomeEnum @testC @testD + } + + union SomeUnion @testA = TypeA | TypeB + + scalar SomeScalar @testA @testB + + enum SomeEnum @testA @testB { + SOME_VALUE @testC + } + + input SomeInput @testA @testB { + some_input_field: String @testC + } + '); + + self::assertEquals([], $schema->validate()); + } + + /** + * @see it('rejects a Schema with directive defined multiple times') + */ + public function testRejectsASchemaWithDirectiveDefinedMultipleTimes() + { + $schema = BuildSchema::build(' + type Query { + test: String + } + + directive @testA on SCHEMA + directive @testA on SCHEMA + '); + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Directive @testA defined multiple times.', + 'locations' => [[ 'line' => 6, 'column' => 11 ], [ 'line' => 7, 'column' => 11 ]], + ], + ] + ); + } + + /** + * @see it('rejects a Schema with same directive used twice per location') + */ + public function testRejectsASchemaWithSameSchemaDirectiveUsedTwice() + { + $schema = BuildSchema::build(' + directive @schema on SCHEMA + directive @object on OBJECT + directive @interface on INTERFACE + directive @union on UNION + directive @scalar on SCALAR + directive @input_object on INPUT_OBJECT + directive @enum on ENUM + directive @field_definition on FIELD_DEFINITION + directive @enum_value on ENUM_VALUE + directive @input_field_definition on INPUT_FIELD_DEFINITION + directive @argument_definition on ARGUMENT_DEFINITION + + schema @schema @schema { + query: Query + } + + type Query implements SomeInterface @object @object { + test(arg: SomeInput @argument_definition @argument_definition): String + } + + interface SomeInterface @interface @interface { + test: String @field_definition @field_definition + } + + union SomeUnion @union @union = Query + + scalar SomeScalar @scalar @scalar + + enum SomeEnum @enum @enum { + SOME_VALUE @enum_value @enum_value + } + + input SomeInput @input_object @input_object { + some_input_field: String @input_field_definition @input_field_definition + } + ', null, ['assumeValid' => true]); + $this->assertMatchesValidationMessage( + $schema->validate(), + [ + [ + 'message' => 'Directive @schema used twice at the same location.', + 'locations' => [[ 'line' => 14, 'column' => 18 ], [ 'line' => 14, 'column' => 26 ]], + ],[ + 'message' => 'Directive @argument_definition used twice at the same location.', + 'locations' => [[ 'line' => 19, 'column' => 33 ], [ 'line' => 19, 'column' => 54 ]], + ],[ + 'message' => 'Directive @object used twice at the same location.', + 'locations' => [[ 'line' => 18, 'column' => 47 ], [ 'line' => 18, 'column' => 55 ]], + ],[ + 'message' => 'Directive @field_definition used twice at the same location.', + 'locations' => [[ 'line' => 23, 'column' => 26 ], [ 'line' => 23, 'column' => 44 ]], + ],[ + 'message' => 'Directive @interface used twice at the same location.', + 'locations' => [[ 'line' => 22, 'column' => 35 ], [ 'line' => 22, 'column' => 46 ]], + ],[ + 'message' => 'Directive @input_field_definition used twice at the same location.', + 'locations' => [[ 'line' => 35, 'column' => 38 ], [ 'line' => 35, 'column' => 62 ]], + ],[ + 'message' => 'Directive @input_object used twice at the same location.', + 'locations' => [[ 'line' => 34, 'column' => 27 ], [ 'line' => 34, 'column' => 41 ]], + ],[ + 'message' => 'Directive @union used twice at the same location.', + 'locations' => [[ 'line' => 26, 'column' => 27 ], [ 'line' => 26, 'column' => 34 ]], + ],[ + 'message' => 'Directive @scalar used twice at the same location.', + 'locations' => [[ 'line' => 28, 'column' => 29 ], [ 'line' => 28, 'column' => 37 ]], + ],[ + 'message' => 'Directive @enum_value used twice at the same location.', + 'locations' => [[ 'line' => 31, 'column' => 24 ], [ 'line' => 31, 'column' => 36 ]], + ],[ + 'message' => 'Directive @enum used twice at the same location.', + 'locations' => [[ 'line' => 30, 'column' => 25 ], [ 'line' => 30, 'column' => 31 ]], + ], + ] + ); + } + + /** + * @see it('rejects a Schema with directive used again in extension') + */ + public function testRejectsASchemaWithSameDefinitionDirectiveUsedTwice() + { + $schema = BuildSchema::build(' + directive @testA on OBJECT + + type Query @testA { + test: String + } + '); + + $extensions = Parser::parse(' + extend type Query @testA + '); + + $extendedSchema = SchemaExtender::extend($schema, $extensions); + + $this->assertMatchesValidationMessage( + $extendedSchema->validate(), + [[ + 'message' => 'Directive @testA used twice at the same location.', + 'locations' => [[ 'line' => 4, 'column' => 22 ], [ 'line' => 2, 'column' => 29 ]], + ], + ] + ); + } + + /** + * @see it('rejects a Schema with directives used in wrong location') + */ + public function testRejectsASchemaWithDirectivesUsedInWrongLocation() + { + $schema = BuildSchema::build(' + directive @schema on SCHEMA + directive @object on OBJECT + directive @interface on INTERFACE + directive @union on UNION + directive @scalar on SCALAR + directive @input_object on INPUT_OBJECT + directive @enum on ENUM + directive @field_definition on FIELD_DEFINITION + directive @enum_value on ENUM_VALUE + directive @input_field_definition on INPUT_FIELD_DEFINITION + directive @argument_definition on ARGUMENT_DEFINITION + + schema @object { + query: Query + } + + type Query implements SomeInterface @schema { + test(arg: SomeInput @field_definition): String + } + + interface SomeInterface @interface { + test: String @argument_definition + } + + union SomeUnion @interface = Query + + scalar SomeScalar @enum_value + + enum SomeEnum @input_object { + SOME_VALUE @enum + } + + input SomeInput @object { + some_input_field: String @union @input_field_definition + } + ', null, ['assumeValid' => true]); + + $extensions = Parser::parse(' + extend type Query @testA + '); + + $extendedSchema = SchemaExtender::extend( + $schema, + $extensions, + ['assumeValid' => true] // TODO: remove this line + ); + + $this->assertMatchesValidationMessage( + $extendedSchema->validate(), + [ + [ + 'message' => 'Directive @object not allowed at SCHEMA location.', + 'locations' => [[ 'line' => 14, 'column' => 18 ], [ 'line' => 3, 'column' => 11 ]], + ], [ + 'message' => 'Directive @field_definition not allowed at ARGUMENT_DEFINITION location.', + 'locations' => [[ 'line' => 19, 'column' => 33 ], [ 'line' => 9, 'column' => 11 ]], + ], [ + 'message' => 'Directive @schema not allowed at OBJECT location.', + 'locations' => [[ 'line' => 18, 'column' => 47 ], [ 'line' => 2, 'column' => 11 ]], + ], [ + 'message' => 'No directive @testA defined.', + 'locations' => [[ 'line' => 2, 'column' => 29 ]], + ], [ + 'message' => 'Directive @argument_definition not allowed at FIELD_DEFINITION location.', + 'locations' => [[ 'line' => 23, 'column' => 26 ], [ 'line' => 12, 'column' => 11 ]], + ], [ + 'message' => 'Directive @union not allowed at INPUT_FIELD_DEFINITION location.', + 'locations' => [[ 'line' => 35, 'column' => 38 ], [ 'line' => 5, 'column' => 11 ]], + ], [ + 'message' => 'Directive @object not allowed at INPUT_OBJECT location.', + 'locations' => [[ 'line' => 34, 'column' => 27 ], [ 'line' => 3, 'column' => 11 ]], + ], [ + 'message' => 'Directive @interface not allowed at UNION location.', + 'locations' => [[ 'line' => 26, 'column' => 27 ], [ 'line' => 4, 'column' => 11 ]], + ], [ + 'message' => 'Directive @enum_value not allowed at SCALAR location.', + 'locations' => [[ 'line' => 28, 'column' => 29 ], [ 'line' => 10, 'column' => 11 ]], + ], [ + 'message' => 'Directive @enum not allowed at ENUM_VALUE location.', + 'locations' => [[ 'line' => 31, 'column' => 24 ], [ 'line' => 8, 'column' => 11 ]], + ], [ + 'message' => 'Directive @input_object not allowed at ENUM location.', + 'locations' => [[ 'line' => 30, 'column' => 25 ], [ 'line' => 7, 'column' => 11 ]], + ], + ] ); - $schema->assertValid(); } } diff --git a/tests/Utils/AssertValidNameTest.php b/tests/Utils/AssertValidNameTest.php index 0876acd80..d4dc80e1a 100644 --- a/tests/Utils/AssertValidNameTest.php +++ b/tests/Utils/AssertValidNameTest.php @@ -12,6 +12,7 @@ class AssertValidNameTest extends TestCase { // Describe: assertValidName() + /** * @see it('throws for use of leading double underscores') */ diff --git a/tests/Utils/AstFromValueTest.php b/tests/Utils/AstFromValueTest.php index b01a73cf7..8626f23d3 100644 --- a/tests/Utils/AstFromValueTest.php +++ b/tests/Utils/AstFromValueTest.php @@ -10,6 +10,7 @@ use GraphQL\Language\AST\IntValueNode; use GraphQL\Language\AST\ListValueNode; use GraphQL\Language\AST\NameNode; +use GraphQL\Language\AST\NodeList; use GraphQL\Language\AST\NullValueNode; use GraphQL\Language\AST\ObjectFieldNode; use GraphQL\Language\AST\ObjectValueNode; @@ -87,7 +88,7 @@ public function testConvertsFloatValuesToIntOrFloatASTs() : void self::assertEquals(new IntValueNode(['value' => '123']), AST::astFromValue(123.0, Type::float())); self::assertEquals(new FloatValueNode(['value' => '123.5']), AST::astFromValue(123.5, Type::float())); self::assertEquals(new IntValueNode(['value' => '10000']), AST::astFromValue(1e4, Type::float())); - self::assertEquals(new FloatValueNode(['value' => '1e+40']), AST::astFromValue(1e40, Type::float())); + self::assertEquals(new FloatValueNode(['value' => '1.0E+40']), AST::astFromValue(1e40, Type::float())); self::assertEquals(new IntValueNode(['value' => '0']), AST::astFromValue(0e40, Type::float())); } @@ -100,7 +101,7 @@ public function testConvertsStringValuesToASTs() : void self::assertEquals(new StringValueNode(['value' => 'VALUE']), AST::astFromValue('VALUE', Type::string())); self::assertEquals(new StringValueNode(['value' => "VA\nLUE"]), AST::astFromValue("VA\nLUE", Type::string())); self::assertEquals(new StringValueNode(['value' => '123']), AST::astFromValue(123, Type::string())); - self::assertEquals(new StringValueNode(['value' => 'false']), AST::astFromValue(false, Type::string())); + self::assertEquals(new StringValueNode(['value' => '']), AST::astFromValue(false, Type::string())); self::assertEquals(new NullValueNode([]), AST::astFromValue(null, Type::string())); self::assertEquals(null, AST::astFromValue(null, Type::nonNull(Type::string()))); } @@ -117,7 +118,6 @@ public function testConvertIdValuesToIntOrStringASTs() : void self::assertEquals(new IntValueNode(['value' => '123']), AST::astFromValue(123, Type::id())); self::assertEquals(new IntValueNode(['value' => '123']), AST::astFromValue('123', Type::id())); self::assertEquals(new StringValueNode(['value' => '01']), AST::astFromValue('01', Type::id())); - self::assertEquals(new StringValueNode(['value' => 'false']), AST::astFromValue(false, Type::id())); self::assertEquals(new NullValueNode([]), AST::astFromValue(null, Type::id())); self::assertEquals(null, AST::astFromValue(null, Type::nonNull(Type::id()))); } @@ -179,18 +179,18 @@ private function complexValue() public function testConvertsArrayValuesToListASTs() : void { $value1 = new ListValueNode([ - 'values' => [ + 'values' => new NodeList([ new StringValueNode(['value' => 'FOO']), new StringValueNode(['value' => 'BAR']), - ], + ]), ]); self::assertEquals($value1, AST::astFromValue(['FOO', 'BAR'], Type::listOf(Type::string()))); $value2 = new ListValueNode([ - 'values' => [ + 'values' => new NodeList([ new EnumValueNode(['value' => 'HELLO']), new EnumValueNode(['value' => 'GOODBYE']), - ], + ]), ]); self::assertEquals($value2, AST::astFromValue(['HELLO', 'GOODBYE'], Type::listOf($this->myEnum()))); } @@ -220,10 +220,10 @@ public function testConvertsInputObjects() : void ]); $expected = new ObjectValueNode([ - 'fields' => [ + 'fields' => new NodeList([ $this->objectField('foo', new IntValueNode(['value' => '3'])), $this->objectField('bar', new EnumValueNode(['value' => 'HELLO'])), - ], + ]), ]); $data = ['foo' => 3, 'bar' => 'HELLO']; @@ -259,9 +259,9 @@ public function testConvertsInputObjectsWithExplicitNulls() : void self::assertEquals( new ObjectValueNode([ - 'fields' => [ + 'fields' => new NodeList([ $this->objectField('foo', new NullValueNode([])), - ], + ]), ]), AST::astFromValue(['foo' => null], $inputObj) ); diff --git a/tests/Utils/AstFromValueUntypedTest.php b/tests/Utils/AstFromValueUntypedTest.php index 2a1049037..bfd5a8cd8 100644 --- a/tests/Utils/AstFromValueUntypedTest.php +++ b/tests/Utils/AstFromValueUntypedTest.php @@ -11,6 +11,7 @@ class AstFromValueUntypedTest extends TestCase { // Describe: valueFromASTUntyped + /** * @see it('parses simple values') */ diff --git a/tests/Utils/BreakingChangesFinderTest.php b/tests/Utils/BreakingChangesFinderTest.php index 898bc2924..5253035cd 100644 --- a/tests/Utils/BreakingChangesFinderTest.php +++ b/tests/Utils/BreakingChangesFinderTest.php @@ -23,7 +23,7 @@ class BreakingChangesFinderTest extends TestCase /** @var ObjectType */ private $queryType; - public function setUp() + public function setUp() : void { $this->queryType = new ObjectType([ 'name' => 'Query', @@ -486,7 +486,7 @@ public function testShouldDetectIfFieldsOnInputTypesChangedKindOrWereRemoved() : } /** - * @see it('should detect if a non-null field is added to an input type') + * @see it('should detect if a required field is added to an input type') */ public function testShouldDetectIfANonNullFieldIsAddedToAnInputType() : void { @@ -500,9 +500,13 @@ public function testShouldDetectIfANonNullFieldIsAddedToAnInputType() : void $newInputType = new InputObjectType([ 'name' => 'InputType1', 'fields' => [ - 'field1' => Type::string(), - 'requiredField' => Type::nonNull(Type::int()), - 'optionalField' => Type::boolean(), + 'field1' => Type::string(), + 'requiredField' => Type::nonNull(Type::int()), + 'optionalField1' => Type::boolean(), + 'optionalField2' => [ + 'type' => Type::nonNull(Type::boolean()), + 'defaultValue' => false, + ], ], ]); @@ -518,8 +522,8 @@ public function testShouldDetectIfANonNullFieldIsAddedToAnInputType() : void $expected = [ [ - 'type' => BreakingChangesFinder::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED, - 'description' => 'A non-null field requiredField on input type InputType1 was added.', + 'type' => BreakingChangesFinder::BREAKING_CHANGE_REQUIRED_INPUT_FIELD_ADDED, + 'description' => 'A required field requiredField on input type InputType1 was added.', ], ]; @@ -889,8 +893,8 @@ public function testShouldDetectIfANonNullFieldArgumentWasAdded() : void $expected = [ [ - 'type' => BreakingChangesFinder::BREAKING_CHANGE_NON_NULL_ARG_ADDED, - 'description' => 'A non-null arg newRequiredArg on Type1.field1 was added', + 'type' => BreakingChangesFinder::BREAKING_CHANGE_REQUIRED_ARG_ADDED, + 'description' => 'A required arg newRequiredArg on Type1.field1 was added', ], ]; @@ -1033,7 +1037,7 @@ public function testShouldDetectInterfacesRemovedFromTypes() : void $expected = [ [ - 'type' => BreakingChangesFinder::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED, 'description' => 'Type1 no longer implements interface Interface1.', ], ]; @@ -1044,6 +1048,54 @@ public function testShouldDetectInterfacesRemovedFromTypes() : void ); } + /** + * @see it('should detect interfaces removed from interfaces') + */ + public function testShouldDetectInterfacesRemovedFromInterfaces() : void + { + $interface1 = new InterfaceType([ + 'name' => 'Interface1', + 'fields' => [ + 'field1' => Type::string(), + ], + ]); + + $oldInterface2 = new InterfaceType([ + 'name' => 'Interface2', + 'fields' => [ + 'field1' => Type::string(), + ], + 'interfaces' => [$interface1], + ]); + $newInterface2 = new InterfaceType([ + 'name' => 'Interface2', + 'fields' => [ + 'field1' => Type::string(), + ], + ]); + + $oldSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [$interface1, $oldInterface2], + ]); + $newSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [$interface1, $newInterface2], + ]); + + $expected = [ + [ + 'type' => BreakingChangesFinder::BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED, + 'description' => 'Interface2 no longer implements interface Interface1.', + ], + ]; + + self::assertEquals( + $expected, + BreakingChangesFinder::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema) + ); + } + /** * @see it('should detect all breaking changes') */ @@ -1177,7 +1229,10 @@ public function testShouldDetectAllBreakingChanges() : void 'name' => 'DirectiveThatRemovesArg', 'locations' => [DirectiveLocation::FIELD_DEFINITION], 'args' => FieldArgument::createMap([ - 'arg1' => ['name' => 'arg1'], + 'arg1' => [ + 'name' => 'arg1', + 'type' => Type::boolean(), + ], ]), ]); $directiveThatRemovesArgNew = new Directive([ @@ -1284,7 +1339,7 @@ public function testShouldDetectAllBreakingChanges() : void 'description' => 'ArgThatChanges.field1 arg id has changed type from Int to String', ], [ - 'type' => BreakingChangesFinder::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED, 'description' => 'TypeThatLosesInterface1 no longer implements interface Interface1.', ], [ @@ -1296,8 +1351,8 @@ public function testShouldDetectAllBreakingChanges() : void 'description' => 'arg1 was removed from DirectiveThatRemovesArg', ], [ - 'type' => BreakingChangesFinder::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED, - 'description' => 'A non-null arg arg1 on directive NonNullDirectiveAdded was added', + 'type' => BreakingChangesFinder::BREAKING_CHANGE_REQUIRED_DIRECTIVE_ARG_ADDED, + 'description' => 'A required arg arg1 on directive NonNullDirectiveAdded was added', ], [ 'type' => BreakingChangesFinder::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED, @@ -1376,7 +1431,10 @@ public function testShouldDetectIfADirectiveArgumentWasRemoved() : void 'name' => 'DirectiveWithArg', 'locations' => [DirectiveLocation::FIELD_DEFINITION], 'args' => FieldArgument::createMap([ - 'arg1' => ['name' => 'arg1'], + 'arg1' => [ + 'name' => 'arg1', + 'type' => Type::string(), + ], ]), ]), ], @@ -1435,8 +1493,8 @@ public function testShouldDetectIfANonNullableDirectiveArgumentWasAdded() : void $expectedBreakingChanges = [ [ - 'type' => BreakingChangesFinder::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED, - 'description' => 'A non-null arg arg1 on directive DirectiveName was added', + 'type' => BreakingChangesFinder::BREAKING_CHANGE_REQUIRED_DIRECTIVE_ARG_ADDED, + 'description' => 'A required arg arg1 on directive DirectiveName was added', ], ]; @@ -1649,7 +1707,7 @@ public function testShouldDetectInterfacesAddedToTypes() : void $expected = [ [ - 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT, + 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED, 'description' => 'Interface1 added to interfaces implemented by Type1.', ], ]; @@ -1660,6 +1718,46 @@ public function testShouldDetectInterfacesAddedToTypes() : void ); } + /** + * @see it('should detect interfaces added to interfaces') + */ + public function testShouldDetectInterfacesAddedToInterfaces() : void + { + $oldInterface = new InterfaceType(['name' => 'OldInterface']); + $newInterface = new InterfaceType(['name' => 'NewInterface']); + + $oldInterface1 = new InterfaceType([ + 'name' => 'Interface1', + 'interfaces' => [$oldInterface], + ]); + $newInterface1 = new InterfaceType([ + 'name' => 'Interface1', + 'interfaces' => [$oldInterface, $newInterface], + ]); + + $oldSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [$oldInterface1], + ]); + + $newSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [$newInterface1], + ]); + + $expected = [ + [ + 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED, + 'description' => 'NewInterface added to interfaces implemented by Interface1.', + ], + ]; + + self::assertEquals( + $expected, + BreakingChangesFinder::findInterfacesAddedToObjectTypes($oldSchema, $newSchema) + ); + } + /** * @see it('should detect if a type was added to a union type') */ @@ -1755,8 +1853,8 @@ public function testShouldDetectIfANullableFieldWasAddedToAnInput() : void $expectedFieldChanges = [ [ - 'description' => 'A nullable field field2 on input type InputType1 was added.', - 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED, + 'description' => 'An optional field field2 on input type InputType1 was added.', + 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_OPTIONAL_INPUT_FIELD_ADDED, ], ]; @@ -1893,7 +1991,7 @@ public function testShouldFindAllDangerousChanges() : void 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM, ], [ - 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT, + 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED, 'description' => 'Interface1 added to interfaces implemented by TypeThatGainsInterface1.', ], [ @@ -1956,8 +2054,8 @@ public function testShouldDetectIfANullableFieldArgumentWasAdded() : void $expectedFieldChanges = [ [ - 'description' => 'A nullable arg arg2 on Type1.field1 was added', - 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_NULLABLE_ARG_ADDED, + 'description' => 'An optional arg arg2 on Type1.field1 was added', + 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_OPTIONAL_ARG_ADDED, ], ]; diff --git a/tests/Utils/BuildClientSchemaTest.php b/tests/Utils/BuildClientSchemaTest.php new file mode 100644 index 000000000..528592bdc --- /dev/null +++ b/tests/Utils/BuildClientSchemaTest.php @@ -0,0 +1,1095 @@ + true]; + + $serverSchema = BuildSchema::build($sdl); + $initialIntrospection = Introspection::fromSchema($serverSchema, $options); + $clientSchema = BuildClientSchema::build($initialIntrospection); + $secondIntrospection = Introspection::fromSchema($clientSchema, $options); + + self::assertSame($initialIntrospection, $secondIntrospection); + } + + /** + * @return array> + */ + protected static function introspectionFromSDL(string $sdl) : array + { + $schema = BuildSchema::build($sdl); + + return Introspection::fromSchema($schema); + } + + protected static function clientSchemaFromSDL(string $sdl) : Schema + { + $introspection = self::introspectionFromSDL($sdl); + + return BuildClientSchema::build($introspection); + } + + // describe('Type System: build schema from introspection', () => { + + /** + * @see it('builds a simple schema', () => { + */ + public function testBuildsASimpleSchema() : void + { + self::assertCycleIntrospection(' + schema { + query: Simple + } + + """This is a simple type""" + type Simple { + """This is a string field""" + string: String + } + '); + } + + /** + * it('builds a schema without the query type', () => { + */ + public function testBuildsASchemaWithoutTheQueryType() : void + { + $sdl = <<getQueryType()); + self::assertSame($sdl, SchemaPrinter::doPrint($clientSchema)); + } + + /** + * it('builds a simple schema with all operation types', () => { + */ + public function testBuildsASimpleSchemaWithAllOperationTypes() : void + { + self::assertCycleIntrospection(' + schema { + query: QueryType + mutation: MutationType + subscription: SubscriptionType + } + + """This is a simple mutation type""" + type MutationType { + """Set the string field""" + string: String + } + + """This is a simple query type""" + type QueryType { + """This is a string field""" + string: String + } + + """This is a simple subscription type""" + type SubscriptionType { + """This is a string field""" + string: String + } + '); + } + + /** + * it('uses built-in scalars when possible', () => { + */ + public function testUsesBuiltInScalarsWhenPossible() : void + { + $sdl = ' + scalar CustomScalar + + type Query { + int: Int + float: Float + string: String + boolean: Boolean + id: ID + custom: CustomScalar + } + '; + + self::assertCycleIntrospection($sdl); + + $schema = BuildSchema::build($sdl); + $introspection = Introspection::fromSchema($schema); + $clientSchema = BuildClientSchema::build($introspection); + + // Built-ins are used + self::assertSame(Type::int(), $clientSchema->getType('Int')); + self::assertSame(Type::float(), $clientSchema->getType('Float')); + self::assertSame(Type::string(), $clientSchema->getType('String')); + self::assertSame(Type::boolean(), $clientSchema->getType('Boolean')); + self::assertSame(Type::id(), $clientSchema->getType('ID')); + + // Custom are built + self::assertNotSame( + $schema->getType('CustomScalar'), + $clientSchema->getType('CustomScalar') + ); + } + + /** + * it('includes standard types only if they are used', () => { + */ + public function testIncludesStandardTypesOnlyIfTheyAreUsed() : void + { + self::markTestSkipped('Introspection currently does not follow the reference implementation.'); + $clientSchema = self::clientSchemaFromSDL(' + type Query { + foo: String + } + '); + + self::assertNull($clientSchema->getType('Int')); + } + + /** + * it('builds a schema with a recursive type reference', () => { + */ + public function testBuildsASchemaWithARecursiveTypeReference() : void + { + self::assertCycleIntrospection(' + schema { + query: Recur + } + + type Recur { + recur: Recur + } + '); + } + + /** + * it('builds a schema with a circular type reference', () => { + */ + public function testBuildsASchemaWithACircularTypeReference() : void + { + self::assertCycleIntrospection(' + type Dog { + bestFriend: Human + } + + type Human { + bestFriend: Dog + } + + type Query { + dog: Dog + human: Human + } + '); + } + + /** + * it('builds a schema with an interface', () => { + */ + public function testBuildsASchemaWithAnInterface() : void + { + self::assertCycleIntrospection(' + type Dog implements Friendly { + bestFriend: Friendly + } + + interface Friendly { + """The best friend of this friendly thing""" + bestFriend: Friendly + } + + type Human implements Friendly { + bestFriend: Friendly + } + + type Query { + friendly: Friendly + } + '); + } + + /** + * it('builds a schema with an interface hierarchy', () => { + */ + public function testBuildsASchemaWithAnInterfaceHierarchy() : void + { + self::assertCycleIntrospection(' + type Dog implements Friendly & Named { + bestFriend: Friendly + name: String + } + + interface Friendly implements Named { + """The best friend of this friendly thing""" + bestFriend: Friendly + name: String + } + + type Human implements Friendly & Named { + bestFriend: Friendly + name: String + } + + interface Named { + name: String + } + + type Query { + friendly: Friendly + } + '); + } + + /** + * it('builds a schema with an implicit interface', () => { + */ + public function testBuildsASchemaWithAnImplicitInterface() : void + { + self::assertCycleIntrospection(' + type Dog implements Friendly { + bestFriend: Friendly + } + + interface Friendly { + """The best friend of this friendly thing""" + bestFriend: Friendly + } + + type Query { + dog: Dog + } + '); + } + + /** + * it('builds a schema with a union', () => { + */ + public function testBuildsASchemaWithAUnion() : void + { + self::assertCycleIntrospection(' + type Dog { + bestFriend: Friendly + } + + union Friendly = Dog | Human + + type Human { + bestFriend: Friendly + } + + type Query { + friendly: Friendly + } + '); + } + + /** + * it('builds a schema with complex field values', () => { + */ + public function testBuildsASchemaWithComplexFieldValues() : void + { + self::assertCycleIntrospection(' + type Query { + string: String + listOfString: [String] + nonNullString: String! + nonNullListOfString: [String]! + nonNullListOfNonNullString: [String!]! + } + '); + } + + /** + * it('builds a schema with field arguments', () => { + */ + public function testBuildsASchemaWithFieldArguments() : void + { + self::assertCycleIntrospection(' + type Query { + """A field with a single arg""" + one( + """This is an int arg""" + intArg: Int + ): String + + """A field with a two args""" + two( + """This is an list of int arg""" + listArg: [Int] + + """This is a required arg""" + requiredArg: Boolean! + ): String + } + '); + } + + /** + * it('builds a schema with default value on custom scalar field', () => { + */ + public function testBuildsASchemaWithDefaultValueOnCustomScalarField() : void + { + self::assertCycleIntrospection(' + scalar CustomScalar + + type Query { + testField(testArg: CustomScalar = "default"): String + } + '); + } + + /** + * it('builds a schema with an enum', () => { + */ + public function testBuildsASchemaWithAnEnum() : void + { + $foodEnum = new EnumType([ + 'name' => 'Food', + 'description' => 'Varieties of food stuffs', + 'values' => [ + 'VEGETABLES' => [ + 'description' => 'Foods that are vegetables', + 'value' => 1, + ], + 'FRUITS' => ['value' => 2], + 'OILS' => [ + 'value' => 3, + 'deprecationReason' => 'Too fatty', + ], + ], + ]); + $schema = new Schema([ + 'query' => new ObjectType([ + 'name' => 'EnumFields', + 'fields' => [ + 'food' => [ + 'description' => 'Repeats the arg you give it', + 'type' => $foodEnum, + 'args' => [ + 'kind' => [ + 'description' => 'what kind of food?', + 'type' => $foodEnum, + ], + ], + ], + ], + ]), + ]); + + $introspection = Introspection::fromSchema($schema); + $clientSchema = BuildClientSchema::build($introspection); + + $introspectionFromClientSchema = Introspection::fromSchema($clientSchema); + self::assertSame($introspection, $introspectionFromClientSchema); + + /** @var EnumType $clientFoodEnum */ + $clientFoodEnum = $clientSchema->getType('Food'); + self::assertInstanceOf(EnumType::class, $clientFoodEnum); + + self::assertCount(3, $clientFoodEnum->getValues()); + + $vegetables = $clientFoodEnum->getValue('VEGETABLES'); + + // Client types do not get server-only values, so `value` mirrors `name`, + // rather than using the integers defined in the "server" schema. + self::assertSame('VEGETABLES', $vegetables->value); + self::assertSame('Foods that are vegetables', $vegetables->description); + self::assertFalse($vegetables->isDeprecated()); + self::assertNull($vegetables->deprecationReason); + self::assertNull($vegetables->astNode); + + $fruits = $clientFoodEnum->getValue('FRUITS'); + self::assertNull($fruits->description); + + $oils = $clientFoodEnum->getValue('OILS'); + self::assertTrue($oils->isDeprecated()); + self::assertSame('Too fatty', $oils->deprecationReason); + } + + /** + * it('builds a schema with an input object', () => { + */ + public function testBuildsASchemaWithAnInputObject() : void + { + self::assertCycleIntrospection(' + """An input address""" + input Address { + """What street is this address?""" + street: String! + + """The city the address is within?""" + city: String! + + """The country (blank will assume USA).""" + country: String = "USA" + } + + type Query { + """Get a geocode from an address""" + geocode( + """The address to lookup""" + address: Address + ): String + } + '); + } + + /** + * it('builds a schema with field arguments with default values', () => { + */ + public function testBuildsASchemaWithFieldArgumentsWithDefaultValues() : void + { + self::assertCycleIntrospection(' + input Geo { + lat: Float + lon: Float + } + + type Query { + defaultInt(intArg: Int = 30): String + defaultList(listArg: [Int] = [1, 2, 3]): String + defaultObject(objArg: Geo = {lat: 37.485, lon: -122.148}): String + defaultNull(intArg: Int = null): String + noDefault(intArg: Int): String + } + '); + } + + /** + * it('builds a schema with custom directives', () => { + */ + public function testBuildsASchemaWithCustomDirectives() : void + { + self::assertCycleIntrospection(' + """This is a custom directive""" + directive @customDirective repeatable on FIELD + + type Query { + string: String + } + '); + } + + /** + * it('builds a schema without directives', () => { + */ + public function testBuildsASchemaWithoutDirectives() : void + { + $sdl = <<getDirectives()); + self::assertSame([], $clientSchema->getDirectives()); + self::assertSame($sdl, SchemaPrinter::doPrint($clientSchema)); + } + + /** + * it('builds a schema aware of deprecation', () => { + */ + public function testBuildsASchemaAwareOfDeprecation() : void + { + self::assertCycleIntrospection(' + enum Color { + """So rosy""" + RED + + """So grassy""" + GREEN + + """So calming""" + BLUE + + """So sickening""" + MAUVE @deprecated(reason: "No longer in fashion") + } + + type Query { + """This is a shiny string field""" + shinyString: String + + """This is a deprecated string field""" + deprecatedString: String @deprecated(reason: "Use shinyString") + color: Color + } + '); + } + + /** + * it('builds a schema with empty deprecation reasons', () => { + */ + public function testBuildsASchemaWithEmptyDeprecationReasons() : void + { + self::assertCycleIntrospection(' + type Query { + someField: String @deprecated(reason: "") + } + + enum SomeEnum { + SOME_VALUE @deprecated(reason: "") + } + '); + } + + /** + * it('can use client schema for limited execution', () => { + */ + public function testUseClientSchemaForLimitedExecution() : void + { + $schema = BuildSchema::build(' + scalar CustomScalar + + type Query { + foo(custom1: CustomScalar, custom2: CustomScalar): String + } + '); + + $introspection = Introspection::fromSchema($schema); + $clientSchema = BuildClientSchema::build($introspection); + + $result = GraphQL::executeQuery( + $clientSchema, + 'query Limited($v: CustomScalar) { foo(custom1: 123, custom2: $v) }', + ['foo' => 'bar', 'unused' => 'value'], + null, + ['v' => 'baz'] + ); + + self::assertSame(['foo' => 'bar'], $result->data); + } + + // describe('throws when given invalid introspection', () => { + + /** + * Construct a default dummy schema that is used in the following tests. + */ + protected static function dummySchema() : Schema + { + return BuildSchema::build(' + type Query { + foo(bar: String): String + } + + interface SomeInterface { + foo: String + } + + union SomeUnion = Query + + enum SomeEnum { FOO } + + input SomeInputObject { + foo: String + } + + directive @SomeDirective on QUERY + '); + } + + protected function _expectExceptionMessage(string $message) : void + { + if (version_compare(Version::id(), '8.4', '<')) { + $this->expectExceptionMessageRegExp($message); + } elseif (method_exists($this, 'expectExceptionMessageMatches')) { + $this->expectExceptionMessageMatches($message); + } + } + + /** + * it('throws when introspection is missing __schema property', () => { + */ + public function testThrowsWhenIntrospectionIsMissingSchemaProperty() : void + { + $this->expectExceptionMessage( + 'Invalid or incomplete introspection result. Ensure that you are passing "data" property of introspection response and no "errors" was returned alongside: [].' + ); + BuildClientSchema::build([]); + } + + /** + * it('throws when referenced unknown type', () => { + */ + public function testThrowsWhenReferencedUnknownType() : void + { + $introspection = Introspection::fromSchema(self::dummySchema()); + + $introspection['__schema']['types'] = array_filter( + $introspection['__schema']['types'], + static function (array $type) : bool { + return $type['name'] !== 'Query'; + } + ); + + $this->expectExceptionMessage( + 'Invalid or incomplete schema, unknown type: Query. Ensure that a full introspection query is used in order to build a client schema.' + ); + BuildClientSchema::build($introspection); + } + + /** + * it('throws when missing definition for one of the standard scalars', () => { + */ + public function testThrowsWhenMissingDefinitionForOneOfTheStandardScalars() : void + { + $schema = BuildSchema::build(' + type Query { + foo: Float + } + '); + $introspection = Introspection::fromSchema($schema); + + $introspection['__schema']['types'] = array_filter( + $introspection['__schema']['types'], + static function (array $type) : bool { + return $type['name'] !== 'Float'; + } + ); + + $this->expectExceptionMessage( + 'Invalid or incomplete schema, unknown type: Float. Ensure that a full introspection query is used in order to build a client schema.' + ); + BuildClientSchema::build($introspection); + } + + /** + * it('throws when type reference is missing name', () => { + */ + public function testThrowsWhenTypeReferenceIsMissingName() : void + { + $introspection = Introspection::fromSchema(self::dummySchema()); + + self::assertNotEmpty($introspection['__schema']['queryType']['name']); + + unset($introspection['__schema']['queryType']['name']); + + $this->expectExceptionMessage('Unknown type reference: [].'); + BuildClientSchema::build($introspection); + } + + /** + * it('throws when missing kind', () => { + */ + public function testThrowsWhenMissingKind() : void + { + $introspection = Introspection::fromSchema(self::dummySchema()); + $queryTypeIntrospection = null; + foreach ($introspection['__schema']['types'] as &$type) { + if ($type['name'] !== 'Query') { + continue; + } + + $queryTypeIntrospection = &$type; + } + + self::assertArrayHasKey('kind', $queryTypeIntrospection); + + unset($queryTypeIntrospection['kind']); + + $this->_expectExceptionMessage( + '/Invalid or incomplete introspection result. Ensure that a full introspection query is used in order to build a client schema: {"name":"Query",.*}\./' + ); + BuildClientSchema::build($introspection); + } + + /** + * it('throws when missing interfaces', () => { + */ + public function testThrowsWhenMissingInterfaces() : void + { + $introspection = Introspection::fromSchema(self::dummySchema()); + $queryTypeIntrospection = null; + foreach ($introspection['__schema']['types'] as &$type) { + if ($type['name'] !== 'Query') { + continue; + } + + $queryTypeIntrospection = &$type; + } + + self::assertArrayHasKey('interfaces', $queryTypeIntrospection); + + unset($queryTypeIntrospection['interfaces']); + + $this->_expectExceptionMessage( + '/Introspection result missing interfaces: {"kind":"OBJECT","name":"Query",.*}\./' + ); + BuildClientSchema::build($introspection); + } + + /** + * it('Legacy support for interfaces with null as interfaces field', () => { + */ + public function testLegacySupportForInterfacesWithNullAsInterfacesField() : void + { + $dummySchema = self::dummySchema(); + $introspection = Introspection::fromSchema($dummySchema); + $queryTypeIntrospection = null; + foreach ($introspection['__schema']['types'] as &$type) { + if ($type['name'] !== 'SomeInterface') { + continue; + } + + $queryTypeIntrospection = &$type; + } + + self::assertArrayHasKey('interfaces', $queryTypeIntrospection); + + $queryTypeIntrospection['interfaces'] = null; + + $clientSchema = BuildClientSchema::build($introspection); + self::assertSame( + SchemaPrinter::doPrint($dummySchema), + SchemaPrinter::doPrint($clientSchema) + ); + } + + /** + * it('throws when missing fields', () => { + */ + public function testThrowsWhenMissingFields() : void + { + $introspection = Introspection::fromSchema(self::dummySchema()); + $queryTypeIntrospection = null; + foreach ($introspection['__schema']['types'] as &$type) { + if ($type['name'] !== 'Query') { + continue; + } + + $queryTypeIntrospection = &$type; + } + + self::assertArrayHasKey('fields', $queryTypeIntrospection); + + unset($queryTypeIntrospection['fields']); + + $this->_expectExceptionMessage( + '/Introspection result missing fields: {"kind":"OBJECT","name":"Query",.*}\./' + ); + BuildClientSchema::build($introspection); + } + + /** + * it('throws when missing field args', () => { + */ + public function testThrowsWhenMissingFieldArgs() : void + { + $introspection = Introspection::fromSchema(self::dummySchema()); + $queryTypeIntrospection = null; + foreach ($introspection['__schema']['types'] as &$type) { + if ($type['name'] !== 'Query') { + continue; + } + + $queryTypeIntrospection = &$type; + } + + $firstField = &$queryTypeIntrospection['fields'][0]; + self::assertArrayHasKey('args', $firstField); + + unset($firstField['args']); + + $this->_expectExceptionMessage( + '/Introspection result missing field args: {"name":"foo",.*}\./' + ); + BuildClientSchema::build($introspection); + } + + /** + * it('throws when output type is used as an arg type', () => { + */ + public function testThrowsWhenOutputTypeIsUsedAsAnArgType() : void + { + $introspection = Introspection::fromSchema(self::dummySchema()); + $queryTypeIntrospection = null; + foreach ($introspection['__schema']['types'] as &$type) { + if ($type['name'] !== 'Query') { + continue; + } + + $queryTypeIntrospection = &$type; + } + + $firstArgType = &$queryTypeIntrospection['fields'][0]['args'][0]['type']; + self::assertArrayHasKey('name', $firstArgType); + + $firstArgType['name'] = 'SomeUnion'; + + $this->expectExceptionMessage( + 'Introspection must provide input type for arguments, but received: "SomeUnion".' + ); + BuildClientSchema::build($introspection); + } + + /** + * it('throws when input type is used as a field type', () => { + */ + public function testThrowsWhenInputTypeIsUsedAsAFieldType() : void + { + $introspection = Introspection::fromSchema(self::dummySchema()); + $queryTypeIntrospection = null; + foreach ($introspection['__schema']['types'] as &$type) { + if ($type['name'] !== 'Query') { + continue; + } + + $queryTypeIntrospection = &$type; + } + + $firstFieldType = &$queryTypeIntrospection['fields'][0]['type']; + self::assertArrayHasKey('name', $firstFieldType); + + $firstFieldType['name'] = 'SomeInputObject'; + + $this->expectExceptionMessage( + 'Introspection must provide output type for fields, but received: "SomeInputObject".' + ); + BuildClientSchema::build($introspection); + } + + /** + * it('throws when missing possibleTypes', () => { + */ + public function testThrowsWhenMissingPossibleTypes() : void + { + $introspection = Introspection::fromSchema(self::dummySchema()); + $someUnionIntrospection = null; + foreach ($introspection['__schema']['types'] as &$type) { + if ($type['name'] !== 'SomeUnion') { + continue; + } + + $someUnionIntrospection = &$type; + } + + self::assertArrayHasKey('possibleTypes', $someUnionIntrospection); + + unset($someUnionIntrospection['possibleTypes']); + + $this->_expectExceptionMessage( + '/Introspection result missing possibleTypes: {"kind":"UNION","name":"SomeUnion",.*}\./' + ); + BuildClientSchema::build($introspection); + } + + /** + * it('throws when missing enumValues', () => { + */ + public function testThrowsWhenMissingEnumValues() : void + { + $introspection = Introspection::fromSchema(self::dummySchema()); + $someEnumIntrospection = null; + foreach ($introspection['__schema']['types'] as &$type) { + if ($type['name'] !== 'SomeEnum') { + continue; + } + + $someEnumIntrospection = &$type; + } + + self::assertArrayHasKey('enumValues', $someEnumIntrospection); + + unset($someEnumIntrospection['enumValues']); + + $this->_expectExceptionMessage( + '/Introspection result missing enumValues: {"kind":"ENUM","name":"SomeEnum",.*}\./' + ); + BuildClientSchema::build($introspection); + } + + /** + * it('throws when missing inputFields', () => { + */ + public function testThrowsWhenMissingInputFields() : void + { + $introspection = Introspection::fromSchema(self::dummySchema()); + $someInputObjectIntrospection = null; + foreach ($introspection['__schema']['types'] as &$type) { + if ($type['name'] !== 'SomeInputObject') { + continue; + } + + $someInputObjectIntrospection = &$type; + } + + self::assertArrayHasKey('inputFields', $someInputObjectIntrospection); + + unset($someInputObjectIntrospection['inputFields']); + + $this->_expectExceptionMessage( + '/Introspection result missing inputFields: {"kind":"INPUT_OBJECT","name":"SomeInputObject",.*}\./' + ); + BuildClientSchema::build($introspection); + } + + /** + * it('throws when missing directive locations', () => { + */ + public function testThrowsWhenMissingDirectiveLocations() : void + { + $introspection = Introspection::fromSchema(self::dummySchema()); + + $someDirectiveIntrospection = &$introspection['__schema']['directives'][0]; + self::assertSame('SomeDirective', $someDirectiveIntrospection['name']); + self::assertSame(['QUERY'], $someDirectiveIntrospection['locations']); + + unset($someDirectiveIntrospection['locations']); + + $this->_expectExceptionMessage( + '/Introspection result missing directive locations: {"name":"SomeDirective",.*}\./' + ); + BuildClientSchema::build($introspection); + } + + /** + * it('throws when missing directive args', () => { + */ + public function testThrowsWhenMissingDirectiveArgs() : void + { + $introspection = Introspection::fromSchema(self::dummySchema()); + + $someDirectiveIntrospection = &$introspection['__schema']['directives'][0]; + self::assertSame('SomeDirective', $someDirectiveIntrospection['name']); + self::assertSame([], $someDirectiveIntrospection['args']); + + unset($someDirectiveIntrospection['args']); + + $this->_expectExceptionMessage( + '/Introspection result missing directive args: {"name":"SomeDirective",.*}\./' + ); + BuildClientSchema::build($introspection); + } + + // describe('very deep decorators are not supported', () => { + + /** + * it('fails on very deep (> 7 levels) lists', () => { + */ + public function testFailsOnVeryDeepListsWithMoreThan7Levels() : void + { + $schema = BuildSchema::build(' + type Query { + foo: [[[[[[[[String]]]]]]]] + } + '); + $introspection = Introspection::fromSchema($schema); + + $this->expectExceptionMessage( + 'Decorated type deeper than introspection query.' + ); + BuildClientSchema::build($introspection); + } + + /** + * it('fails on very deep (> 7 levels) non-null', () => { + */ + public function testFailsOnVeryDeepNonNullWithMoreThan7Levels() : void + { + $schema = BuildSchema::build(' + type Query { + foo: [[[[String!]!]!]!] + } + '); + $introspection = Introspection::fromSchema($schema); + + $this->expectExceptionMessage( + 'Decorated type deeper than introspection query.' + ); + BuildClientSchema::build($introspection); + } + + /** + * it('succeeds on deep (<= 7 levels) types', () => { + */ + public function testSucceedsOnDeepTypesWithMoreThanOrEqualTo7Levels() : void + { + // e.g., fully non-null 3D matrix + self::assertCycleIntrospection(' + type Query { + foo: [[[String!]!]!]! + } + '); + } + + // describe('prevents infinite recursion on invalid introspection', () => { + + /** + * it('recursive interfaces', () => { + */ + public function testRecursiveInterfaces() : void + { + $sdl = ' + type Query { + foo: Foo + } + + type Foo implements Foo { + foo: String + } + '; + $schema = BuildSchema::build($sdl); + $introspection = Introspection::fromSchema($schema); + + $this->expectExceptionMessage('Expected Foo to be a GraphQL Interface type.'); + BuildClientSchema::build($introspection); + } + + /** + * it('recursive union', () => { + */ + public function testRecursiveUnion() : void + { + $sdl = ' + type Query { + foo: Foo + } + + union Foo = Foo + '; + $schema = BuildSchema::build($sdl); + $introspection = Introspection::fromSchema($schema); + + $this->expectExceptionMessage('Expected Foo to be a GraphQL Object type.'); + BuildClientSchema::build($introspection); + } +} diff --git a/tests/Utils/BuildSchemaTest.php b/tests/Utils/BuildSchemaTest.php index e8e328029..6ac487691 100644 --- a/tests/Utils/BuildSchemaTest.php +++ b/tests/Utils/BuildSchemaTest.php @@ -5,6 +5,7 @@ namespace GraphQL\Tests\Utils; use Closure; +use GraphQL\Error\DebugFlag; use GraphQL\Error\Error; use GraphQL\GraphQL; use GraphQL\Language\AST\EnumTypeDefinitionNode; @@ -12,9 +13,14 @@ use GraphQL\Language\AST\ObjectTypeDefinitionNode; use GraphQL\Language\Parser; use GraphQL\Language\Printer; +use GraphQL\Tests\PHPUnit\ArraySubsetAsserts; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; +use GraphQL\Type\Definition\InputObjectType; +use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; +use GraphQL\Type\Definition\ScalarType; +use GraphQL\Type\Definition\UnionType; use GraphQL\Utils\BuildSchema; use GraphQL\Utils\SchemaPrinter; use PHPUnit\Framework\TestCase; @@ -23,7 +29,10 @@ class BuildSchemaTest extends TestCase { + use ArraySubsetAsserts; + // Describe: Schema Builder + /** * @see it('can use built schema for limited execution') */ @@ -36,7 +45,7 @@ public function testUseBuiltSchemaForLimitedExecution() : void ')); $result = GraphQL::executeQuery($schema, '{ str }', ['str' => 123]); - self::assertEquals(['str' => 123], $result->toArray(true)['data']); + self::assertEquals(['str' => 123], $result->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)['data']); } /** @@ -51,7 +60,7 @@ public function testBuildSchemaDirectlyFromSource() : void '); $root = [ - 'add' => static function ($root, $args) { + 'add' => static function ($rootValue, $args) { return $args['x'] + $args['y']; }, ]; @@ -61,7 +70,7 @@ public function testBuildSchemaDirectlyFromSource() : void '{ add(x: 34, y: 55) }', $root ); - self::assertEquals(['data' => ['add' => 89]], $result->toArray(true)); + self::assertEquals(['data' => ['add' => 89]], $result->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)); } /** @@ -98,6 +107,8 @@ public function testWithDirectives() : void $body = ' directive @foo(arg: Int) on FIELD +directive @repeatableFoo(arg: Int) repeatable on FIELD + type Query { str: String } @@ -333,6 +344,32 @@ interface WorldInterface { self::assertEquals($output, $body); } + /** + * @see it('Simple interface heirarchy') + */ + public function testSimpleInterfaceHeirarchy() : void + { + $body = ' +schema { + query: Child +} + +interface Child implements Parent { + str: String +} + +type Hello implements Parent & Child { + str: String +} + +interface Parent { + str: String +} +'; + $output = $this->cycleOutput($body); + self::assertEquals($output, $body); + } + /** * @see it('Simple output enum') */ @@ -432,12 +469,28 @@ public function testMultipleUnion() : void self::assertEquals($output, $body); } + /** + * @see it('Can build recursive Union') + */ + public function testCanBuildRecursiveUnion() + { + $schema = BuildSchema::build(' + union Hello = Hello + + type Query { + hello: Hello + } + '); + $errors = $schema->validate(); + self::assertNotEmpty($errors); + } + /** * @see it('Specifying Union type using __typename') */ public function testSpecifyingUnionTypeUsingTypename() : void { - $schema = BuildSchema::buildAST(Parser::parse(' + $schema = BuildSchema::buildAST(Parser::parse(' type Query { fruits: [Fruit] } @@ -452,7 +505,7 @@ public function testSpecifyingUnionTypeUsingTypename() : void length: Int } ')); - $query = ' + $query = ' { fruits { ... on Apple { @@ -464,7 +517,7 @@ public function testSpecifyingUnionTypeUsingTypename() : void } } '; - $root = [ + $rootValue = [ 'fruits' => [ [ 'color' => 'green', @@ -476,7 +529,7 @@ public function testSpecifyingUnionTypeUsingTypename() : void ], ], ]; - $expected = [ + $expected = [ 'data' => [ 'fruits' => [ ['color' => 'green'], @@ -485,8 +538,8 @@ public function testSpecifyingUnionTypeUsingTypename() : void ], ]; - $result = GraphQL::executeQuery($schema, $query, $root); - self::assertEquals($expected, $result->toArray(true)); + $result = GraphQL::executeQuery($schema, $query, $rootValue); + self::assertEquals($expected, $result->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)); } /** @@ -494,7 +547,7 @@ public function testSpecifyingUnionTypeUsingTypename() : void */ public function testSpecifyingInterfaceUsingTypename() : void { - $schema = BuildSchema::buildAST(Parser::parse(' + $schema = BuildSchema::buildAST(Parser::parse(' type Query { characters: [Character] } @@ -513,7 +566,7 @@ interface Character { primaryFunction: String } ')); - $query = ' + $query = ' { characters { name @@ -526,7 +579,7 @@ interface Character { } } '; - $root = [ + $rootValue = [ 'characters' => [ [ 'name' => 'Han Solo', @@ -540,7 +593,7 @@ interface Character { ], ], ]; - $expected = [ + $expected = [ 'data' => [ 'characters' => [ ['name' => 'Han Solo', 'totalCredits' => 10], @@ -549,8 +602,8 @@ interface Character { ], ]; - $result = GraphQL::executeQuery($schema, $query, $root); - self::assertEquals($expected, $result->toArray(true)); + $result = GraphQL::executeQuery($schema, $query, $rootValue); + self::assertEquals($expected, $result->toArray(DebugFlag::INCLUDE_DEBUG_MESSAGE)); } /** @@ -689,6 +742,28 @@ interface Iface { self::assertEquals($output, $body); } + /** + * @see it('Unreferenced interface implementing referenced interface') + */ + public function testUnreferencedInterfaceImplementingReferencedInterface() : void + { + $body = ' +interface Child implements Parent { + key: String +} + +interface Parent { + key: String +} + +type Query { + iface: Parent +} +'; + $output = $this->cycleOutput($body); + self::assertEquals($output, $body); + } + /** * @see it('Unreferenced type implementing referenced union') */ @@ -747,7 +822,9 @@ enum: MyEnum self::assertTrue($otherValue->isDeprecated()); self::assertEquals('Terrible reasons', $otherValue->deprecationReason); - $rootFields = $schema->getType('Query')->getFields(); + /** @var ObjectType $queryType */ + $queryType = $schema->getType('Query'); + $rootFields = $queryType->getFields(); self::assertEquals($rootFields['field1']->isDeprecated(), true); self::assertEquals($rootFields['field1']->deprecationReason, 'No longer supported'); @@ -792,13 +869,20 @@ interfaceField: String directive @test(arg: TestScalar) on FIELD '); $schema = BuildSchema::buildAST($schemaAST); + /** @var ObjectType $query */ - $query = $schema->getType('Query'); - $testInput = $schema->getType('TestInput'); - $testEnum = $schema->getType('TestEnum'); - $testUnion = $schema->getType('TestUnion'); + $query = $schema->getType('Query'); + /** @var InputObjectType $testInput */ + $testInput = $schema->getType('TestInput'); + /** @var EnumType $testEnum */ + $testEnum = $schema->getType('TestEnum'); + /** @var UnionType $testUnion */ + $testUnion = $schema->getType('TestUnion'); + /** @var InterfaceType $testInterface */ $testInterface = $schema->getType('TestInterface'); - $testType = $schema->getType('TestType'); + /** @var ObjectType $testType */ + $testType = $schema->getType('TestType'); + /** @var ScalarType $testScalar */ $testScalar = $schema->getType('TestScalar'); $testDirective = $schema->getDirective('test'); @@ -883,32 +967,39 @@ public function testCanBuildInvalidSchema() : void self::assertGreaterThan(0, $errors); } - // Describe: Failures - /** - * @see it('Allows only a single schema definition') + * @see it('Rejects invalid SDL') */ - public function testAllowsOnlySingleSchemaDefinition() : void + public function testRejectsInvalidSDL() { + $doc = Parser::parse(' + type Query { + foo: String @unknown + } + '); $this->expectException(Error::class); - $this->expectExceptionMessage('Must provide only one schema definition.'); - $body = ' -schema { - query: Hello -} - -schema { - query: Hello -} + $this->expectExceptionMessage('Unknown directive "unknown".'); + BuildSchema::build($doc); + } -type Hello { - bar: Bar -} -'; - $doc = Parser::parse($body); - BuildSchema::buildAST($doc); + /** + * @see it('Allows to disable SDL validation') + */ + public function testAllowsToDisableSDLValidation() + { + $body = ' + type Query { + foo: String @unknown + } + '; + // Should not throw: + BuildSchema::build($body, null, ['assumeValid' => true]); + BuildSchema::build($body, null, ['assumeValidSDL' => true]); + self::assertTrue(true); } + // Describe: Failures + /** * @see it('Allows only a single query type') */ @@ -923,7 +1014,7 @@ public function testAllowsOnlySingleQueryType() : void } type Hello { - bar: Bar + bar: String } type Yellow { @@ -949,7 +1040,7 @@ public function testAllowsOnlySingleMutationType() : void } type Hello { - bar: Bar + bar: String } type Yellow { @@ -975,7 +1066,7 @@ public function testAllowsOnlySingleSubscriptionType() : void } type Hello { - bar: Bar + bar: String } type Yellow { @@ -1107,6 +1198,23 @@ public function testUnknownSubscriptionType() : void BuildSchema::buildAST($doc); } + /** + * @see it('Does not consider directive names') + */ + public function testDoesNotConsiderDirectiveNames() + { + $body = ' + schema { + query: Foo + } + + directive @Foo on QUERY + '; + $doc = Parser::parse($body); + $this->expectExceptionMessage('Specified query type "Foo" not found in document.'); + BuildSchema::build($doc); + } + /** * @see it('Does not consider operation names') */ @@ -1241,7 +1349,8 @@ interface Hello { self::assertEquals('Hello', $defaultConfig['name']); self::assertInstanceOf(Closure::class, $defaultConfig['fields']); self::assertArrayHasKey('description', $defaultConfig); - self::assertCount(4, $defaultConfig); + self::assertArrayHasKey('interfaces', $defaultConfig); + self::assertCount(5, $defaultConfig); self::assertEquals(array_keys($allNodesMap), ['Query', 'Color', 'Hello']); self::assertEquals('My description of Hello', $schema->getType('Hello')->description); } diff --git a/tests/Utils/CoerceValueTest.php b/tests/Utils/CoerceValueTest.php index 831c98f66..342a0d4d1 100644 --- a/tests/Utils/CoerceValueTest.php +++ b/tests/Utils/CoerceValueTest.php @@ -10,6 +10,9 @@ use GraphQL\Utils\Utils; use GraphQL\Utils\Value; use PHPUnit\Framework\TestCase; +use function acos; +use function log; +use function pow; class CoerceValueTest extends TestCase { @@ -19,7 +22,7 @@ class CoerceValueTest extends TestCase /** @var InputObjectType */ private $testInputObject; - public function setUp() + public function setUp() : void { $this->testEnum = new EnumType([ 'name' => 'TestEnum', @@ -40,19 +43,42 @@ public function setUp() /** * Describe: coerceValue + */ + + /** + * Describe: for GraphQLString * - * @see it('coercing an array to GraphQLString produces an error') + * @see it('returns error for array input as string') */ public function testCoercingAnArrayToGraphQLStringProducesAnError() : void { $result = Value::coerceValue([1, 2, 3], Type::string()); - $this->expectError( + $this->expectGraphQLError( $result, - 'Expected type String; String cannot represent an array value: [1,2,3]' + 'Expected type String; String cannot represent a non string value: [1,2,3]' ); self::assertEquals( - 'String cannot represent an array value: [1,2,3]', + 'String cannot represent a non string value: [1,2,3]', + $result['errors'][0]->getPrevious()->getMessage() + ); + } + + /** + * Describe: for GraphQLID + * + * @see it('returns error for array input as ID') + */ + public function testCoercingAnArrayToGraphQLIDProducesAnError() : void + { + $result = Value::coerceValue([1, 2, 3], Type::id()); + $this->expectGraphQLError( + $result, + 'Expected type ID; ID cannot represent value: [1,2,3]' + ); + + self::assertEquals( + 'ID cannot represent value: [1,2,3]', $result['errors'][0]->getPrevious()->getMessage() ); } @@ -60,67 +86,89 @@ public function testCoercingAnArrayToGraphQLStringProducesAnError() : void /** * Describe: for GraphQLInt */ - private function expectError($result, $expected) + private function expectGraphQLError($result, $expected) { - self::assertInternalType('array', $result); - self::assertInternalType('array', $result['errors']); + self::assertIsArray($result); + self::assertIsArray($result['errors']); self::assertCount(1, $result['errors']); self::assertEquals($expected, $result['errors'][0]->getMessage()); self::assertEquals(Utils::undefined(), $result['value']); } /** - * @see it('returns no error for int input') + * @see it('returns value for integer') */ public function testIntReturnsNoErrorForIntInput() : void + { + $result = Value::coerceValue(1, Type::int()); + $this->expectValue($result, 1); + } + + /** + * @see it('returns error for numeric looking string') + */ + public function testReturnsErrorForNumericLookingString() { $result = Value::coerceValue('1', Type::int()); - $this->expectNoErrors($result); + $this->expectGraphQLError($result, 'Expected type Int; Int cannot represent non-integer value: 1'); } - private function expectNoErrors($result) + private function expectValue($result, $expected) { - self::assertInternalType('array', $result); - self::assertNull($result['errors']); + self::assertIsArray($result); + self::assertEquals(null, $result['errors']); self::assertNotEquals(Utils::undefined(), $result['value']); + self::assertEquals($expected, $result['value']); } /** - * @see it('returns no error for negative int input') + * @see it('returns value for negative int input') */ public function testIntReturnsNoErrorForNegativeIntInput() : void { - $result = Value::coerceValue('-1', Type::int()); - $this->expectNoErrors($result); + $result = Value::coerceValue(-1, Type::int()); + $this->expectValue($result, -1); } /** - * @see it('returns no error for exponent input') + * @see it('returns value for exponent input') */ public function testIntReturnsNoErrorForExponentInput() : void { - $result = Value::coerceValue('1e3', Type::int()); - $this->expectNoErrors($result); + $result = Value::coerceValue(1e3, Type::int()); + $this->expectValue($result, 1000); } /** - * @see it('returns no error for null') + * @see it('returns null for null value') */ public function testIntReturnsASingleErrorNull() : void { $result = Value::coerceValue(null, Type::int()); - $this->expectNoErrors($result); + $this->expectValue($result, null); } /** - * @see it('returns a single error for empty value') + * @see it('returns a single error for empty string as value') */ public function testIntReturnsASingleErrorForEmptyValue() : void { $result = Value::coerceValue('', Type::int()); - $this->expectError( + $this->expectGraphQLError( + $result, + 'Expected type Int; Int cannot represent non-integer value: (empty string)' + ); + } + + /** + * @see it('returns a single error for 2^32 input as int') + */ + public function testReturnsASingleErrorFor2x32InputAsInt() + { + $result = Value::coerceValue(pow(2, 32), Type::int()); + $this->expectGraphQLError( $result, - 'Expected type Int; Int cannot represent non 32-bit signed integer value: (empty string)' + 'Expected type Int; Int cannot represent non 32-bit signed integer value: 4294967296' ); } @@ -129,24 +177,45 @@ public function testIntReturnsASingleErrorForEmptyValue() : void */ public function testIntReturnsErrorForFloatInputAsInt() : void { - $result = Value::coerceValue('1.5', Type::int()); - $this->expectError( + $result = Value::coerceValue(1.5, Type::int()); + $this->expectGraphQLError( $result, 'Expected type Int; Int cannot represent non-integer value: 1.5' ); } - // Describe: for GraphQLFloat + /** + * @see it('returns a single error for Infinity input as int') + */ + public function testReturnsASingleErrorForInfinityInputAsInt() + { + $inf = log(0); + $result = Value::coerceValue($inf, Type::int()); + $this->expectGraphQLError( + $result, + 'Expected type Int; Int cannot represent non 32-bit signed integer value: -INF' + ); + } + + public function testReturnsASingleErrorForNaNInputAsInt() + { + $nan = acos(8); + $result = Value::coerceValue($nan, Type::int()); + $this->expectGraphQLError( + $result, + 'Expected type Int; Int cannot represent non-integer value: NAN' + ); + } /** - * @see it('returns a single error for char input') + * @see it('returns a single error for string input') */ public function testIntReturnsASingleErrorForCharInput() : void { $result = Value::coerceValue('a', Type::int()); - $this->expectError( + $this->expectGraphQLError( $result, - 'Expected type Int; Int cannot represent non 32-bit signed integer value: a' + 'Expected type Int; Int cannot represent non-integer value: a' ); } @@ -156,60 +225,97 @@ public function testIntReturnsASingleErrorForCharInput() : void public function testIntReturnsASingleErrorForMultiCharInput() : void { $result = Value::coerceValue('meow', Type::int()); - $this->expectError( + $this->expectGraphQLError( $result, - 'Expected type Int; Int cannot represent non 32-bit signed integer value: meow' + 'Expected type Int; Int cannot represent non-integer value: meow' ); } + // Describe: for GraphQLFloat + /** - * @see it('returns no error for int input') + * @see it('returns value for integer') */ public function testFloatReturnsNoErrorForIntInput() : void { - $result = Value::coerceValue('1', Type::float()); - $this->expectNoErrors($result); + $result = Value::coerceValue(1, Type::float()); + $this->expectValue($result, 1); + } + + /** + * @see it('returns value for decimal') + */ + public function testReturnsValueForDecimal() + { + $result = Value::coerceValue(1.1, Type::float()); + $this->expectValue($result, 1.1); } /** - * @see it('returns no error for exponent input') + * @see it('returns value for exponent input') */ public function testFloatReturnsNoErrorForExponentInput() : void { - $result = Value::coerceValue('1e3', Type::float()); - $this->expectNoErrors($result); + $result = Value::coerceValue(1e3, Type::float()); + $this->expectValue($result, 1000); } /** - * @see it('returns no error for float input') + * @see it('returns error for numeric looking string') */ - public function testFloatReturnsNoErrorForFloatInput() : void + public function testFloatReturnsErrorForNumericLookingString() { - $result = Value::coerceValue('1.5', Type::float()); - $this->expectNoErrors($result); + $result = Value::coerceValue('1', Type::float()); + $this->expectGraphQLError( + $result, + 'Expected type Float; Float cannot represent non numeric value: 1' + ); } /** - * @see it('returns no error for null') + * @see it('returns null for null value') */ public function testFloatReturnsASingleErrorNull() : void { $result = Value::coerceValue(null, Type::float()); - $this->expectNoErrors($result); + $this->expectValue($result, null); } /** - * @see it('returns a single error for empty value') + * @see it('returns a single error for empty string input') */ public function testFloatReturnsASingleErrorForEmptyValue() : void { $result = Value::coerceValue('', Type::float()); - $this->expectError( + $this->expectGraphQLError( $result, 'Expected type Float; Float cannot represent non numeric value: (empty string)' ); } + /** + * @see it('returns a single error for Infinity input') + */ + public function testFloatReturnsASingleErrorForInfinityInput() : void + { + $inf = log(0); + $result = Value::coerceValue($inf, Type::float()); + $this->expectGraphQLError( + $result, + 'Expected type Float; Float cannot represent non numeric value: -INF' + ); + } + + public function testFloatReturnsASingleErrorForNaNInput() : void + { + $nan = acos(8); + $result = Value::coerceValue($nan, Type::float()); + $this->expectGraphQLError( + $result, + 'Expected type Float; Float cannot represent non numeric value: NAN' + ); + } + // DESCRIBE: for GraphQLEnum /** @@ -218,7 +324,7 @@ public function testFloatReturnsASingleErrorForEmptyValue() : void public function testFloatReturnsASingleErrorForCharInput() : void { $result = Value::coerceValue('a', Type::float()); - $this->expectError( + $this->expectGraphQLError( $result, 'Expected type Float; Float cannot represent non numeric value: a' ); @@ -230,7 +336,7 @@ public function testFloatReturnsASingleErrorForCharInput() : void public function testFloatReturnsASingleErrorForMultiCharInput() : void { $result = Value::coerceValue('meow', Type::float()); - $this->expectError( + $this->expectGraphQLError( $result, 'Expected type Float; Float cannot represent non numeric value: meow' ); @@ -242,12 +348,10 @@ public function testFloatReturnsASingleErrorForMultiCharInput() : void public function testReturnsNoErrorForAKnownEnumName() : void { $fooResult = Value::coerceValue('FOO', $this->testEnum); - $this->expectNoErrors($fooResult); - self::assertEquals('InternalFoo', $fooResult['value']); + $this->expectValue($fooResult, 'InternalFoo'); $barResult = Value::coerceValue('BAR', $this->testEnum); - $this->expectNoErrors($barResult); - self::assertEquals(123456789, $barResult['value']); + $this->expectValue($barResult, 123456789); } // DESCRIBE: for GraphQLInputObject @@ -258,7 +362,7 @@ public function testReturnsNoErrorForAKnownEnumName() : void public function testReturnsErrorForMisspelledEnumValue() : void { $result = Value::coerceValue('foo', $this->testEnum); - $this->expectError($result, 'Expected type TestEnum; did you mean FOO?'); + $this->expectGraphQLError($result, 'Expected type TestEnum; did you mean FOO?'); } /** @@ -267,10 +371,10 @@ public function testReturnsErrorForMisspelledEnumValue() : void public function testReturnsErrorForIncorrectValueType() : void { $result1 = Value::coerceValue(123, $this->testEnum); - $this->expectError($result1, 'Expected type TestEnum.'); + $this->expectGraphQLError($result1, 'Expected type TestEnum.'); $result2 = Value::coerceValue(['field' => 'value'], $this->testEnum); - $this->expectError($result2, 'Expected type TestEnum.'); + $this->expectGraphQLError($result2, 'Expected type TestEnum.'); } /** @@ -279,8 +383,7 @@ public function testReturnsErrorForIncorrectValueType() : void public function testReturnsNoErrorForValidInput() : void { $result = Value::coerceValue(['foo' => 123], $this->testInputObject); - $this->expectNoErrors($result); - self::assertEquals(['foo' => 123], $result['value']); + $this->expectValue($result, ['foo' => 123]); } /** @@ -289,7 +392,13 @@ public function testReturnsNoErrorForValidInput() : void public function testReturnsErrorForNonObjectType() : void { $result = Value::coerceValue(123, $this->testInputObject); - $this->expectError($result, 'Expected type TestInputObject to be an object.'); + $this->expectGraphQLError($result, 'Expected type TestInputObject to be an object.'); + } + + public function testReturnsNoErrorForStdClassInput() : void + { + $result = Value::coerceValue((object) ['foo' => 123], $this->testInputObject); + $this->expectValue($result, ['foo' => 123]); } /** @@ -298,9 +407,9 @@ public function testReturnsErrorForNonObjectType() : void public function testReturnErrorForAnInvalidField() : void { $result = Value::coerceValue(['foo' => 'abc'], $this->testInputObject); - $this->expectError( + $this->expectGraphQLError( $result, - 'Expected type Int at value.foo; Int cannot represent non 32-bit signed integer value: abc' + 'Expected type Int at value.foo; Int cannot represent non-integer value: abc' ); } @@ -312,8 +421,8 @@ public function testReturnsMultipleErrorsForMultipleInvalidFields() : void $result = Value::coerceValue(['foo' => 'abc', 'bar' => 'def'], $this->testInputObject); self::assertEquals( [ - 'Expected type Int at value.foo; Int cannot represent non 32-bit signed integer value: abc', - 'Expected type Int at value.bar; Int cannot represent non 32-bit signed integer value: def', + 'Expected type Int at value.foo; Int cannot represent non-integer value: abc', + 'Expected type Int at value.bar; Int cannot represent non-integer value: def', ], $result['errors'] ); @@ -325,7 +434,7 @@ public function testReturnsMultipleErrorsForMultipleInvalidFields() : void public function testReturnsErrorForAMissingRequiredField() : void { $result = Value::coerceValue(['bar' => 123], $this->testInputObject); - $this->expectError($result, 'Field value.foo of required type Int! was not provided.'); + $this->expectGraphQLError($result, 'Field value.foo of required type Int! was not provided.'); } /** @@ -334,7 +443,7 @@ public function testReturnsErrorForAMissingRequiredField() : void public function testReturnsErrorForAnUnknownField() : void { $result = Value::coerceValue(['foo' => 123, 'unknownField' => 123], $this->testInputObject); - $this->expectError($result, 'Field "unknownField" is not defined by type TestInputObject.'); + $this->expectGraphQLError($result, 'Field "unknownField" is not defined by type TestInputObject.'); } /** @@ -343,6 +452,6 @@ public function testReturnsErrorForAnUnknownField() : void public function testReturnsErrorForAMisspelledField() : void { $result = Value::coerceValue(['foo' => 123, 'bart' => 123], $this->testInputObject); - $this->expectError($result, 'Field "bart" is not defined by type TestInputObject; did you mean bar?'); + $this->expectGraphQLError($result, 'Field "bart" is not defined by type TestInputObject; did you mean bar?'); } } diff --git a/tests/Utils/ExtractTypesTest.php b/tests/Utils/ExtractTypesTest.php index f2a30bc91..be2de6dfe 100644 --- a/tests/Utils/ExtractTypesTest.php +++ b/tests/Utils/ExtractTypesTest.php @@ -54,7 +54,7 @@ class ExtractTypesTest extends TestCase /** @var InputObjectType */ private $postCommentMutationInput; - public function setUp() + public function setUp() : void { $this->node = new InterfaceType([ 'name' => 'Node', @@ -65,7 +65,7 @@ public function setUp() $this->content = new InterfaceType([ 'name' => 'Content', - 'fields' => function () { + 'fields' => function () : array { return [ 'title' => Type::string(), 'body' => Type::string(), @@ -82,7 +82,7 @@ public function setUp() $this->node, $this->content, ], - 'fields' => function () { + 'fields' => function () : array { return [ $this->node->getField('id'), $this->content->getField('title'), @@ -100,7 +100,7 @@ public function setUp() $this->node, $this->content, ], - 'fields' => function () { + 'fields' => function () : array { return [ 'id' => $this->node->getField('id'), 'title' => $this->content->getField('title'), @@ -119,7 +119,7 @@ public function setUp() $this->node, $this->content, ], - 'fields' => function () { + 'fields' => function () : array { return [ 'id' => $this->node->getField('id'), 'title' => $this->content->getField('title'), @@ -145,7 +145,7 @@ public function setUp() 'interfaces' => [ $this->node, ], - 'fields' => function () { + 'fields' => function () : array { return [ 'id' => $this->node->getField('id'), 'author' => $this->user, @@ -162,7 +162,7 @@ public function setUp() 'interfaces' => [ $this->node, ], - 'fields' => function () { + 'fields' => function () : array { return [ 'id' => $this->node->getField('id'), 'name' => Type::string(), @@ -175,7 +175,7 @@ public function setUp() 'interfaces' => [ $this->node, ], - 'fields' => function () { + 'fields' => function () : array { return [ 'id' => $this->node->getField('id'), 'name' => Type::string(), diff --git a/tests/Utils/IsValidLiteralValueTest.php b/tests/Utils/IsValidLiteralValueTest.php index e97271247..d5f5491f3 100644 --- a/tests/Utils/IsValidLiteralValueTest.php +++ b/tests/Utils/IsValidLiteralValueTest.php @@ -13,6 +13,7 @@ class IsValidLiteralValueTest extends TestCase { // DESCRIBE: isValidLiteralValue + /** * @see it('Returns no errors for a valid value') */ diff --git a/tests/Utils/MixedStoreTest.php b/tests/Utils/MixedStoreTest.php index 42e64d7c5..f4e4e19d5 100644 --- a/tests/Utils/MixedStoreTest.php +++ b/tests/Utils/MixedStoreTest.php @@ -14,7 +14,7 @@ class MixedStoreTest extends TestCase /** @var MixedStore */ private $mixedStore; - public function setUp() + public function setUp() : void { $this->mixedStore = new MixedStore(); } @@ -38,7 +38,7 @@ public function getPossibleValues() 'a', [], new stdClass(), - static function () { + static function () : void { }, new MixedStore(), ]; @@ -66,7 +66,7 @@ private function assertProvidesArrayAccess($key, $value) self::assertFalse(isset($this->mixedStore[$key]), $err); $this->mixedStore[$key] = $value; self::assertTrue(isset($this->mixedStore[$key]), $err); - self::assertEquals(! empty($value), ! empty($this->mixedStore[$key]), $err); + self::assertEquals((bool) $value, (bool) $this->mixedStore[$key], $err); self::assertSame($value, $this->mixedStore[$key], $err); unset($this->mixedStore[$key]); self::assertFalse(isset($this->mixedStore[$key]), $err); @@ -124,7 +124,7 @@ public function testAcceptsObjectKeys() : void $this->assertAcceptsKeyValue(new stdClass(), $value); $this->assertAcceptsKeyValue(new MixedStore(), $value); $this->assertAcceptsKeyValue( - static function () { + static function () : void { }, $value ); diff --git a/tests/Utils/QuotedOrListTest.php b/tests/Utils/QuotedOrListTest.php index 53e8f69cb..9be1f6fe3 100644 --- a/tests/Utils/QuotedOrListTest.php +++ b/tests/Utils/QuotedOrListTest.php @@ -11,6 +11,7 @@ class QuotedOrListTest extends TestCase { // DESCRIBE: quotedOrList + /** * @see it('Does not accept an empty list') */ diff --git a/tests/Utils/SchemaExtenderTest.php b/tests/Utils/SchemaExtenderTest.php index 341a38142..1b8dc4c0b 100644 --- a/tests/Utils/SchemaExtenderTest.php +++ b/tests/Utils/SchemaExtenderTest.php @@ -6,6 +6,7 @@ use GraphQL\Error\Error; use GraphQL\GraphQL; +use GraphQL\Language\AST\DefinitionNode; use GraphQL\Language\AST\DocumentNode; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NodeList; @@ -54,7 +55,7 @@ class SchemaExtenderTest extends TestCase /** @var Directive */ protected $FooDirective; - public function setUp() + public function setUp() : void { parent::setUp(); @@ -67,21 +68,31 @@ public function setUp() $SomeInterfaceType = new InterfaceType([ 'name' => 'SomeInterface', - 'fields' => static function () use (&$SomeInterfaceType) { + 'fields' => static function () use (&$SomeInterfaceType) : array { return [ - 'name' => [ 'type' => Type::string()], 'some' => [ 'type' => $SomeInterfaceType], ]; }, ]); + $AnotherInterfaceType = new InterfaceType([ + 'name' => 'AnotherInterface', + 'interfaces' => [$SomeInterfaceType], + 'fields' => static function () use (&$AnotherInterfaceType) : array { + return [ + 'name' => [ 'type' => Type::string()], + 'some' => [ 'type' => $AnotherInterfaceType], + ]; + }, + ]); + $FooType = new ObjectType([ 'name' => 'Foo', - 'interfaces' => [$SomeInterfaceType], - 'fields' => static function () use ($SomeInterfaceType, &$FooType) { + 'interfaces' => [$AnotherInterfaceType, $SomeInterfaceType], + 'fields' => static function () use ($AnotherInterfaceType, &$FooType) : array { return [ 'name' => [ 'type' => Type::string() ], - 'some' => [ 'type' => $SomeInterfaceType ], + 'some' => [ 'type' => $AnotherInterfaceType ], 'tree' => [ 'type' => Type::nonNull(Type::listOf($FooType))], ]; }, @@ -92,7 +103,6 @@ public function setUp() 'interfaces' => [$SomeInterfaceType], 'fields' => static function () use ($SomeInterfaceType, $FooType) : array { return [ - 'name' => [ 'type' => Type::string() ], 'some' => [ 'type' => $SomeInterfaceType ], 'foo' => [ 'type' => $FooType ], ]; @@ -183,7 +193,7 @@ public function setUp() $testSchemaAst = Parser::parse(SchemaPrinter::doPrint($this->testSchema)); - $this->testSchemaDefinitions = array_map(static function ($node) { + $this->testSchemaDefinitions = array_map(static function ($node) : string { return Printer::doPrint($node); }, iterator_to_array($testSchemaAst->definitions->getIterator())); @@ -203,13 +213,14 @@ protected function dedent(string $str) : string } /** - * @param mixed[]|null $options + * @param array $options */ - protected function extendTestSchema(string $sdl, ?array $options = null) : Schema + protected function extendTestSchema(string $sdl, array $options = []) : Schema { $originalPrint = SchemaPrinter::doPrint($this->testSchema); $ast = Parser::parse($sdl); $extendedSchema = SchemaExtender::extend($this->testSchema, $ast, $options); + self::assertEquals(SchemaPrinter::doPrint($this->testSchema), $originalPrint); return $extendedSchema; @@ -217,13 +228,17 @@ protected function extendTestSchema(string $sdl, ?array $options = null) : Schem protected function printTestSchemaChanges(Schema $extendedSchema) : string { - $ast = Parser::parse(SchemaPrinter::doPrint($extendedSchema)); - $ast->definitions = array_values(array_filter( - $ast->definitions instanceof NodeList ? iterator_to_array($ast->definitions->getIterator()) : $ast->definitions, + $ast = Parser::parse(SchemaPrinter::doPrint($extendedSchema)); + /** @var array $extraDefinitions */ + $extraDefinitions = array_values(array_filter( + iterator_to_array($ast->definitions->getIterator()), function (Node $node) : bool { return ! in_array(Printer::doPrint($node), $this->testSchemaDefinitions, true); } )); + /** @phpstan-var NodeList $definitionNodeList */ + $definitionNodeList = new NodeList($extraDefinitions); + $ast->definitions = $definitionNodeList; return Printer::doPrint($ast); } @@ -247,8 +262,8 @@ public function testExtendsWithoutAlteringOriginalSchema() newField: String }'); self::assertNotEquals($extendedSchema, $this->testSchema); - self::assertContains('newField', SchemaPrinter::doPrint($extendedSchema)); - self::assertNotContains('newField', SchemaPrinter::doPrint($this->testSchema)); + self::assertStringContainsString('newField', SchemaPrinter::doPrint($extendedSchema)); + self::assertStringNotContainsString('newField', SchemaPrinter::doPrint($this->testSchema)); } /** @@ -340,9 +355,9 @@ public function testExtendsObjectsByAddingNewFields() self::assertEquals( $this->printTestSchemaChanges($extendedSchema), $this->dedent(' - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newField: String } @@ -517,7 +532,7 @@ interfaceField: String type TestType implements TestInterface { interfaceField: String } - directive @test(arg: Int) on FIELD | SCALAR + directive @test(arg: Int) repeatable on FIELD | SCALAR '); $extendedTwiceSchema = SchemaExtender::extend($extendedSchema, $ast); @@ -776,9 +791,9 @@ public function testExtendsObjectsByAddingNewFieldsWithArguments() self::assertEquals( $this->dedent(' - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newField(arg1: String, arg2: NewInputObj!): String } @@ -806,9 +821,9 @@ public function testExtendsObjectsByAddingNewFieldsWithExistingTypes() self::assertEquals( $this->dedent(' - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newField(arg1: SomeEnum!): SomeEnum } @@ -880,9 +895,9 @@ enum NewEnum { self::assertEquals( $this->dedent(' - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newObject: NewObject newInterface: NewInterface @@ -934,9 +949,9 @@ interface NewInterface { self::assertEquals( $this->dedent(' - type Foo implements SomeInterface & NewInterface { + type Foo implements AnotherInterface & SomeInterface & NewInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! baz: String } @@ -1054,6 +1069,10 @@ public function testExtendsInterfacesByAddingNewFields() extend interface SomeInterface { newField: String } + + extend interface AnotherInterface { + newField: String + } extend type Bar { newField: String @@ -1066,22 +1085,26 @@ public function testExtendsInterfacesByAddingNewFields() self::assertEquals( $this->dedent(' - type Bar implements SomeInterface { + interface AnotherInterface implements SomeInterface { name: String + some: AnotherInterface + newField: String + } + + type Bar implements SomeInterface { some: SomeInterface foo: Foo newField: String } - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newField: String } interface SomeInterface { - name: String some: SomeInterface newField: String } @@ -1090,6 +1113,48 @@ interface SomeInterface { ); } + /** + * @see it('extends interfaces by adding new implemted interfaces') + */ + public function testExtendsInterfacesByAddingNewImplementedInterfaces() + { + $extendedSchema = $this->extendTestSchema(' + interface NewInterface { + newField: String + } + + extend interface AnotherInterface implements NewInterface { + newField: String + } + + extend type Foo implements NewInterface { + newField: String + } + '); + + self::assertEquals( + $this->dedent(' + interface AnotherInterface implements SomeInterface & NewInterface { + name: String + some: AnotherInterface + newField: String + } + + type Foo implements AnotherInterface & SomeInterface & NewInterface { + name: String + some: AnotherInterface + tree: [Foo]! + newField: String + } + + interface NewInterface { + newField: String + } + '), + $this->printTestSchemaChanges($extendedSchema) + ); + } + /** * @see it('allows extension of interface with missing Object fields') */ @@ -1107,7 +1172,6 @@ public function testAllowsExtensionOfInterfaceWithMissingObjectFields() self::assertEquals( $this->dedent(' interface SomeInterface { - name: String some: SomeInterface newField: String } @@ -1125,6 +1189,7 @@ public function testExtendsInterfacesMultipleTimes() extend interface SomeInterface { newFieldA: Int } + extend interface SomeInterface { newFieldB(test: Boolean): String } @@ -1133,7 +1198,6 @@ public function testExtendsInterfacesMultipleTimes() self::assertEquals( $this->dedent(' interface SomeInterface { - name: String some: SomeInterface newFieldA: Int newFieldB(test: Boolean): String @@ -1151,19 +1215,19 @@ public function testMayExtendMutationsAndSubscriptions() $mutationSchema = new Schema([ 'query' => new ObjectType([ 'name' => 'Query', - 'fields' => static function () { + 'fields' => static function () : array { return [ 'queryField' => [ 'type' => Type::string() ] ]; }, ]), 'mutation' => new ObjectType([ 'name' => 'Mutation', - 'fields' => static function () { + 'fields' => static function () : array { return [ 'mutationField' => ['type' => Type::string() ] ]; }, ]), 'subscription' => new ObjectType([ 'name' => 'Subscription', - 'fields' => static function () { + 'fields' => static function () : array { return ['subscriptionField' => ['type' => Type::string()]]; }, ]), @@ -1185,6 +1249,7 @@ public function testMayExtendMutationsAndSubscriptions() $originalPrint = SchemaPrinter::doPrint($mutationSchema); $extendedSchema = SchemaExtender::extend($mutationSchema, $ast); + self::assertNotEquals($mutationSchema, $extendedSchema); self::assertEquals(SchemaPrinter::doPrint($mutationSchema), $originalPrint); self::assertEquals(SchemaPrinter::doPrint($extendedSchema), $this->dedent(' @@ -1258,7 +1323,7 @@ public function testSetsCorrectDescriptionUsingLegacyComments() public function testMayExtendDirectivesWithNewComplexDirective() { $extendedSchema = $this->extendTestSchema(' - directive @profile(enable: Boolean! tag: String) on QUERY | FIELD + directive @profile(enable: Boolean! tag: String) repeatable on QUERY | FIELD '); $extendedDirective = $extendedSchema->getDirective('profile'); @@ -1290,12 +1355,10 @@ public function testRejectsInvalidSDL() extend schema @unknown '; - try { - $this->extendTestSchema($sdl); - self::fail(); - } catch (Error $error) { - self::assertEquals('Unknown directive "unknown".', $error->getMessage()); - } + $this->expectException(Error::class); + $this->expectExceptionMessage('Unknown directive "unknown".'); + + $this->extendTestSchema($sdl); } /** @@ -1354,7 +1417,7 @@ public function testDoesNotAllowReplacingACustomDirective() */ public function testDoesNotAllowReplacingAnExistingType() { - $existingTypeError = static function ($type) { + $existingTypeError = static function ($type) : string { return 'Type "' . $type . '" already exists in the schema. It cannot also be defined in this type definition.'; }; @@ -1430,7 +1493,7 @@ enum SomeEnum */ public function testDoesNotAllowReplacingAnExistingField() { - $existingFieldError = static function (string $type, string $field) { + $existingFieldError = static function (string $type, string $field) : string { return 'Field "' . $type . '.' . $field . '" already exists in the schema. It cannot also be defined in this type extension.'; }; @@ -1575,68 +1638,6 @@ public function testDoesNotAllowExtendingAnUnknownType() } } - /** - * @see it('maintains configuration of the original schema object') - */ - public function testMaintainsConfigurationOfTheOriginalSchemaObject() - { - self::markTestSkipped('allowedLegacyNames currently not supported'); - - $testSchemaWithLegacyNames = new Schema( - [ - 'query' => new ObjectType([ - 'name' => 'Query', - 'fields' => static function () { - return ['id' => ['type' => Type::id()]]; - }, - ]), - ]/*, - [ 'allowedLegacyNames' => ['__badName'] ] - */ - ); - - $ast = Parser::parse(' - extend type Query { - __badName: String - } - '); - $schema = SchemaExtender::extend($testSchemaWithLegacyNames, $ast); - self::assertEquals(['__badName'], $schema->__allowedLegacyNames); - } - - /** - * @see it('adds to the configuration of the original schema object') - */ - public function testAddsToTheConfigurationOfTheOriginalSchemaObject() - { - self::markTestSkipped('allowedLegacyNames currently not supported'); - - $testSchemaWithLegacyNames = new Schema( - [ - 'query' => new ObjectType([ - 'name' => 'Query', - 'fields' => static function () { - return ['__badName' => ['type' => Type::string()]]; - }, - ]), - ]/*, - ['allowedLegacyNames' => ['__badName']] - */ - ); - - $ast = Parser::parse(' - extend type Query { - __anotherBadName: String - } - '); - - $schema = SchemaExtender::extend($testSchemaWithLegacyNames, $ast, [ - 'allowedLegacyNames' => ['__anotherBadName'], - ]); - - self::assertEquals(['__badName', '__anotherBadName'], $schema->__allowedLegacyNames); - } - /** * @see it('does not allow extending a mismatch type') */ @@ -1845,7 +1846,7 @@ public function testSchemaExtensionASTAreAvailableFromSchemaObject() '), implode( "\n", - array_map(static function ($node) { + array_map(static function ($node) : string { return Printer::doPrint($node) . "\n"; }, $nodes) ) @@ -1938,7 +1939,7 @@ public function testOriginalResolversArePreserved() 'fields' => [ 'hello' => [ 'type' => Type::string(), - 'resolve' => static function () { + 'resolve' => static function () : string { return 'Hello World!'; }, ], @@ -1956,7 +1957,39 @@ public function testOriginalResolversArePreserved() $extendedSchema = SchemaExtender::extend($schema, $documentNode); $helloResolveFn = $extendedSchema->getQueryType()->getField('hello')->resolveFn; - self::assertInternalType('callable', $helloResolveFn); + self::assertIsCallable($helloResolveFn); + + $query = '{ hello }'; + $result = GraphQL::executeQuery($extendedSchema, $query); + self::assertSame(['data' => ['hello' => 'Hello World!']], $result->toArray()); + } + + public function testOriginalResolveFieldIsPreserved() + { + $queryType = new ObjectType([ + 'name' => 'Query', + 'fields' => [ + 'hello' => [ + 'type' => Type::string(), + ], + ], + 'resolveField' => static function () : string { + return 'Hello World!'; + }, + ]); + + $schema = new Schema(['query' => $queryType]); + + $documentNode = Parser::parse(' +extend type Query { + misc: String +} +'); + + $extendedSchema = SchemaExtender::extend($schema, $documentNode); + $queryResolveFieldFn = $extendedSchema->getQueryType()->resolveFieldFn; + + self::assertIsCallable($queryResolveFieldFn); $query = '{ hello }'; $result = GraphQL::executeQuery($extendedSchema, $query); diff --git a/tests/Utils/SchemaPrinterTest.php b/tests/Utils/SchemaPrinterTest.php index afc851ba4..cd874e613 100644 --- a/tests/Utils/SchemaPrinterTest.php +++ b/tests/Utils/SchemaPrinterTest.php @@ -4,6 +4,7 @@ namespace GraphQL\Tests\Utils; +use Generator; use GraphQL\Language\DirectiveLocation; use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\Directive; @@ -21,6 +22,7 @@ class SchemaPrinterTest extends TestCase { // Describe: Type System Printer + /** * @see it('Prints String Field') */ @@ -147,6 +149,47 @@ public function testPrintNonNullArrayNonNullStringField() : void ); } + /** + * @see it('Prints Field With "@deprecated" Directive') + * + * @dataProvider deprecationReasonDataProvider + */ + public function testPrintDeprecatedField(?string $deprecationReason, string $expectedDeprecationDirective) : void + { + $output = $this->printSingleFieldSchema([ + 'type' => Type::int(), + 'deprecationReason' => $deprecationReason, + ]); + self::assertSame( + ' +type Query { + singleField: Int' . $expectedDeprecationDirective . ' +} +', + $output + ); + } + + public function deprecationReasonDataProvider() : Generator + { + yield 'when deprecationReason is null' => [ + null, + '', + ]; + yield 'when deprecationReason is empty string' => [ + '', + ' @deprecated', + ]; + yield 'when deprecationReason is the default deprecation reason' => [ + Directive::DEFAULT_DEPRECATION_REASON, + ' @deprecated', + ]; + yield 'when deprecationReason is not empty string' => [ + 'this is deprecated', + ' @deprecated(reason: "this is deprecated")', + ]; + } + /** * @see it('Print Object Field') */ @@ -481,6 +524,68 @@ interface Foo { str: String } +type Query { + bar: Bar +} +', + $output + ); + } + + /** + * @see it('Print Hierarchical Interface') + */ + public function testPrintHierarchicalInterface() : void + { + $FooType = new InterfaceType([ + 'name' => 'Foo', + 'fields' => ['str' => ['type' => Type::string()]], + ]); + + $BaazType = new InterfaceType([ + 'name' => 'Baaz', + 'interfaces' => [$FooType], + 'fields' => [ + 'int' => ['type' => Type::int()], + 'str' => ['type' => Type::string()], + ], + ]); + + $BarType = new ObjectType([ + 'name' => 'Bar', + 'fields' => [ + 'str' => ['type' => Type::string()], + 'int' => ['type' => Type::int()], + ], + 'interfaces' => [$FooType, $BaazType], + ]); + + $query = new ObjectType([ + 'name' => 'Query', + 'fields' => ['bar' => ['type' => $BarType]], + ]); + + $schema = new Schema([ + 'query' => $query, + 'types' => [$BarType], + ]); + $output = $this->printForTest($schema); + self::assertEquals( + ' +interface Baaz implements Foo { + int: Int + str: String +} + +type Bar implements Foo & Baaz { + str: String + int: Int +} + +interface Foo { + str: String +} + type Query { bar: Bar } @@ -663,26 +768,50 @@ public function testPrintsCustomDirectives() : void $query = new ObjectType([ 'name' => 'Query', 'fields' => [ - 'field' => ['type' => Type::string()], + 'field' => [ + 'type' => Type::string(), + ], ], ]); - $customDirectives = new Directive([ - 'name' => 'customDirective', + $simpleDirective = new Directive([ + 'name' => 'simpleDirective', 'locations' => [ DirectiveLocation::FIELD, ], ]); + $complexDirective = new Directive([ + 'name' => 'complexDirective', + 'description' => 'Complex Directive', + 'args' => [ + 'stringArg' => [ + 'type' => Type::string(), + ], + 'intArg' => [ + 'type' => Type::int(), + 'defaultValue' => -1, + ], + ], + 'isRepeatable' => true, + 'locations' => [ + DirectiveLocation::FIELD, + DirectiveLocation::QUERY, + ], + ]); + $schema = new Schema([ 'query' => $query, - 'directives' => [$customDirectives], + 'directives' => [$simpleDirective, $complexDirective], ]); $output = $this->printForTest($schema); self::assertEquals( ' -directive @customDirective on FIELD +directive @simpleDirective on FIELD + +"""Complex Directive""" +directive @complexDirective(stringArg: String, intArg: Int = -1) repeatable on FIELD | QUERY type Query { field: String @@ -713,6 +842,7 @@ public function testOneLinePrintsAShortDescription() : void $output ); + /** @var ObjectType $recreatedRoot */ $recreatedRoot = BuildSchema::build($output)->getTypeMap()['Query']; $recreatedField = $recreatedRoot->getFields()['singleField']; self::assertEquals($description, $recreatedField->description); @@ -741,6 +871,7 @@ public function testDoesNotOneLinePrintADescriptionThatEndsWithAQuote() : void $output ); + /** @var ObjectType $recreatedRoot */ $recreatedRoot = BuildSchema::build($output)->getTypeMap()['Query']; $recreatedField = $recreatedRoot->getFields()['singleField']; self::assertEquals($description, $recreatedField->description); @@ -768,6 +899,7 @@ public function testPReservesLeadingSpacesWhenPrintingADescription() : void $output ); + /** @var ObjectType $recreatedRoot */ $recreatedRoot = BuildSchema::build($output)->getTypeMap()['Query']; $recreatedField = $recreatedRoot->getFields()['singleField']; self::assertEquals($description, $recreatedField->description); @@ -808,8 +940,8 @@ public function testPrintIntrospectionSchema() : void directive @deprecated( """ Explains why this element was deprecated, usually also including a suggestion - for how to access supported similar data. Formatted in - [Markdown](https://daringfireball.net/projects/markdown/). + for how to access supported similar data. Formatted using the Markdown syntax + (as specified by [CommonMark](https://commonmark.org/). """ reason: String = "No longer supported" ) on FIELD_DEFINITION | ENUM_VALUE @@ -825,11 +957,9 @@ public function testPrintIntrospectionSchema() : void type __Directive { name: String! description: String - locations: [__DirectiveLocation!]! args: [__InputValue!]! - onOperation: Boolean! @deprecated(reason: "Use `locations`.") - onFragment: Boolean! @deprecated(reason: "Use `locations`.") - onField: Boolean! @deprecated(reason: "Use `locations`.") + isRepeatable: Boolean! + locations: [__DirectiveLocation!]! } """ @@ -858,6 +988,9 @@ enum __DirectiveLocation { """Location adjacent to an inline fragment.""" INLINE_FRAGMENT + """Location adjacent to a variable definition.""" + VARIABLE_DEFINITION + """Location adjacent to a schema definition.""" SCHEMA @@ -992,7 +1125,7 @@ enum __TypeKind { OBJECT """ - Indicates this type is an interface. `fields` and `possibleTypes` are valid fields. + Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields. """ INTERFACE @@ -1051,8 +1184,8 @@ public function testPrintIntrospectionSchemaWithCommentDescriptions() : void # Marks an element of a GraphQL schema as no longer supported. directive @deprecated( # Explains why this element was deprecated, usually also including a suggestion - # for how to access supported similar data. Formatted in - # [Markdown](https://daringfireball.net/projects/markdown/). + # for how to access supported similar data. Formatted using the Markdown syntax + # (as specified by [CommonMark](https://commonmark.org/). reason: String = "No longer supported" ) on FIELD_DEFINITION | ENUM_VALUE @@ -1065,11 +1198,9 @@ public function testPrintIntrospectionSchemaWithCommentDescriptions() : void type __Directive { name: String! description: String - locations: [__DirectiveLocation!]! args: [__InputValue!]! - onOperation: Boolean! @deprecated(reason: "Use `locations`.") - onFragment: Boolean! @deprecated(reason: "Use `locations`.") - onField: Boolean! @deprecated(reason: "Use `locations`.") + isRepeatable: Boolean! + locations: [__DirectiveLocation!]! } # A Directive can be adjacent to many parts of the GraphQL language, a @@ -1096,6 +1227,9 @@ enum __DirectiveLocation { # Location adjacent to an inline fragment. INLINE_FRAGMENT + # Location adjacent to a variable definition. + VARIABLE_DEFINITION + # Location adjacent to a schema definition. SCHEMA @@ -1211,7 +1345,7 @@ enum __TypeKind { # Indicates this type is an object. `fields` and `interfaces` are valid fields. OBJECT - # Indicates this type is an interface. `fields` and `possibleTypes` are valid fields. + # Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields. INTERFACE # Indicates this type is a union. `possibleTypes` is a valid field. diff --git a/tests/Utils/SuggestionListTest.php b/tests/Utils/SuggestionListTest.php index ae72b2e61..6f5f10fcb 100644 --- a/tests/Utils/SuggestionListTest.php +++ b/tests/Utils/SuggestionListTest.php @@ -10,6 +10,7 @@ class SuggestionListTest extends TestCase { // DESCRIBE: suggestionList + /** * @see it('Returns results when input is empty') */ @@ -39,7 +40,7 @@ public function testReturnsOptionsSortedBasedOnSimilarity() : void { self::assertEquals( Utils::suggestionList('abc', ['a', 'ab', 'abc']), - ['abc', 'ab'] + ['abc', 'ab', 'a'] ); } } diff --git a/tests/Utils/ValueFromAstTest.php b/tests/Utils/ValueFromAstTest.php index ee3f6a843..310df55f1 100644 --- a/tests/Utils/ValueFromAstTest.php +++ b/tests/Utils/ValueFromAstTest.php @@ -183,7 +183,7 @@ public function testCoercesInputObjectsAccordingToInputCoercionRules() : void private function inputObj() { - return $this->inputObj ?: $this->inputObj = new InputObjectType([ + return $this->inputObj ?? $this->inputObj = new InputObjectType([ 'name' => 'TestInput', 'fields' => [ 'int' => ['type' => Type::int(), 'defaultValue' => 42], diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php index b91238255..8d6c1782e 100644 --- a/tests/UtilsTest.php +++ b/tests/UtilsTest.php @@ -8,6 +8,7 @@ use InvalidArgumentException; use PHPUnit\Framework\TestCase; use stdClass; +use function mb_check_encoding; class UtilsTest extends TestCase { @@ -20,4 +21,39 @@ public function testAssignThrowsExceptionOnMissingRequiredKey() : void $this->expectExceptionMessage('Key requiredKey is expected to be set and not to be null'); Utils::assign($object, [], ['requiredKey']); } + + /** + * @param int $input + * @param string $expected + * + * @dataProvider chrUtf8DataProvider + */ + public function testChrUtf8Generation($input, $expected) : void + { + $result = Utils::chr($input); + self::assertTrue(mb_check_encoding($result, 'UTF-8')); + self::assertEquals($expected, $result); + } + + public function chrUtf8DataProvider() + { + return [ + 'alphabet' => [ + 'input' => 0x0061, + 'expected' => 'a', + ], + 'numeric' => [ + 'input' => 0x0030, + 'expected' => '0', + ], + 'between 128 and 256' => [ + 'input' => 0x00E9, + 'expected' => 'é', + ], + 'emoji' => [ + 'input' => 0x231A, + 'expected' => '⌚', + ], + ]; + } } diff --git a/tests/Validator/DisableIntrospectionTest.php b/tests/Validator/DisableIntrospectionTest.php index f5d61b9eb..dddceabe1 100644 --- a/tests/Validator/DisableIntrospectionTest.php +++ b/tests/Validator/DisableIntrospectionTest.php @@ -11,6 +11,7 @@ class DisableIntrospectionTest extends ValidatorTestCase { // Validate: Disable Introspection + /** * @see it('fails if the query contains __schema') */ diff --git a/tests/Validator/ExecutableDefinitionsTest.php b/tests/Validator/ExecutableDefinitionsTest.php index 07517553f..524fbbde1 100644 --- a/tests/Validator/ExecutableDefinitionsTest.php +++ b/tests/Validator/ExecutableDefinitionsTest.php @@ -11,6 +11,7 @@ class ExecutableDefinitionsTest extends ValidatorTestCase { // Validate: Executable definitions + /** * @see it('with only operation') */ diff --git a/tests/Validator/FieldsOnCorrectTypeTest.php b/tests/Validator/FieldsOnCorrectTypeTest.php index 057ae8b2c..7daef09f5 100644 --- a/tests/Validator/FieldsOnCorrectTypeTest.php +++ b/tests/Validator/FieldsOnCorrectTypeTest.php @@ -11,6 +11,7 @@ class FieldsOnCorrectTypeTest extends ValidatorTestCase { // Validate: Fields on correct type + /** * @see it('Object field selection') */ diff --git a/tests/Validator/FragmentsOnCompositeTypesTest.php b/tests/Validator/FragmentsOnCompositeTypesTest.php index deec5c4a2..50ad1712a 100644 --- a/tests/Validator/FragmentsOnCompositeTypesTest.php +++ b/tests/Validator/FragmentsOnCompositeTypesTest.php @@ -11,6 +11,7 @@ class FragmentsOnCompositeTypesTest extends ValidatorTestCase { // Validate: Fragments on composite types + /** * @see it('object is valid fragment type') */ @@ -58,6 +59,23 @@ public function testObjectIsValidInlineFragmentType() : void ); } + /** + * @see it('interface is valid inline fragment type') + */ + public function testInterfaceIsValidInlineFragmentType() : void + { + $this->expectPassesRule( + new FragmentsOnCompositeTypes(), + ' + fragment validFragment on Mammal { + ... on Canine { + name + } + } + ' + ); + } + /** * @see it('inline fragment without type is valid') */ diff --git a/tests/Validator/KnownArgumentNamesTest.php b/tests/Validator/KnownArgumentNamesTest.php index 4c3c163f4..d29f92b76 100644 --- a/tests/Validator/KnownArgumentNamesTest.php +++ b/tests/Validator/KnownArgumentNamesTest.php @@ -6,11 +6,14 @@ use GraphQL\Error\FormattedError; use GraphQL\Language\SourceLocation; +use GraphQL\Utils\BuildSchema; use GraphQL\Validator\Rules\KnownArgumentNames; +use GraphQL\Validator\Rules\KnownArgumentNamesOnDirectives; class KnownArgumentNamesTest extends ValidatorTestCase { // Validate: Known argument names: + /** * @see it('single arg is known') */ @@ -146,7 +149,7 @@ public function testUndirectiveArgsAreInvalid() : void private function unknownDirectiveArg($argName, $directiveName, $suggestedArgs, $line, $column) { return FormattedError::create( - KnownArgumentNames::unknownDirectiveArgMessage($argName, $directiveName, $suggestedArgs), + KnownArgumentNamesOnDirectives::unknownDirectiveArgMessage($argName, $directiveName, $suggestedArgs), [new SourceLocation($line, $column)] ); } @@ -259,4 +262,140 @@ public function testUnknownArgsDeeply() : void ] ); } + + // within SDL: + + /** + * @see it('known arg on directive defined inside SDL') + */ + public function testKnownArgOnDirectiveDefinedInsideSDL() + { + $this->expectPassesRule( + new KnownArgumentNamesOnDirectives(), + ' + type Query { + foo: String @test(arg: "") + } + + directive @test(arg: String) on FIELD_DEFINITION + ' + ); + } + + /** + * @see it('unknown arg on directive defined inside SDL') + */ + public function testUnknownArgOnDirectiveDefinedInsideSDL() + { + $this->expectFailsRule( + new KnownArgumentNamesOnDirectives(), + ' + type Query { + foo: String @test(unknown: "") + } + + directive @test(arg: String) on FIELD_DEFINITION + ', + [ + $this->unknownDirectiveArg('unknown', 'test', [], 3, 37), + ] + ); + } + + /** + * @see it('misspelled arg name is reported on directive defined inside SDL') + */ + public function testMisspelledArgNameIsReportedOnDirectiveDefinedInsideSDL() + { + $this->expectFailsRule( + new KnownArgumentNamesOnDirectives(), + ' + type Query { + foo: String @test(agr: "") + } + + directive @test(arg: String) on FIELD_DEFINITION + ', + [$this->unknownDirectiveArg('agr', 'test', ['arg'], 3, 37)] + ); + } + + /** + * @see it('unknown arg on standard directive') + */ + public function testUnknownArgOnStandardDirective() + { + $this->expectFailsRule( + new KnownArgumentNamesOnDirectives(), + ' + type Query { + foo: String @deprecated(unknown: "") + } + ', + [$this->unknownDirectiveArg('unknown', 'deprecated', [], 3, 43)] + ); + } + + /** + * @see it('unknown arg on overrided standard directive') + */ + public function testUnknownArgOnOverriddenStandardDirective() + { + $this->expectFailsRule( + new KnownArgumentNamesOnDirectives(), + ' + type Query { + foo: String @deprecated(reason: "") + } + directive @deprecated(arg: String) on FIELD + ', + [$this->unknownDirectiveArg('reason', 'deprecated', [], 3, 43)] + ); + } + + /** + * @see it('unknown arg on directive defined in schema extension') + */ + public function testUnknownArgOnDirectiveDefinedInSchemaExtension() + { + $schema = BuildSchema::build(' + type Query { + foo: String + } + '); + $sdl = ' + directive @test(arg: String) on OBJECT + + extend type Query @test(unknown: "") + '; + $this->expectInvalid( + $schema, + [new KnownArgumentNamesOnDirectives()], + $sdl, + [$this->unknownDirectiveArg('unknown', 'test', [], 4, 38)] + ); + } + + /** + * @see it('unknown arg on directive used in schema extension') + */ + public function testUnknownArgOnDirectiveUsedInSchemaExtension() + { + $schema = BuildSchema::build(' + directive @test(arg: String) on OBJECT + + type Query { + foo: String + } + '); + $sdl = ' + extend type Query @test(unknown: "") + '; + $this->expectInvalid( + $schema, + [new KnownArgumentNamesOnDirectives()], + $sdl, + [$this->unknownDirectiveArg('unknown', 'test', [], 2, 37)] + ); + } } diff --git a/tests/Validator/KnownDirectivesTest.php b/tests/Validator/KnownDirectivesTest.php index 014e892e0..38c190ea0 100644 --- a/tests/Validator/KnownDirectivesTest.php +++ b/tests/Validator/KnownDirectivesTest.php @@ -6,11 +6,39 @@ use GraphQL\Error\FormattedError; use GraphQL\Language\SourceLocation; +use GraphQL\Type\Schema; +use GraphQL\Utils\BuildSchema; use GraphQL\Validator\Rules\KnownDirectives; class KnownDirectivesTest extends ValidatorTestCase { + /** @var Schema */ + public $schemaWithSDLDirectives; + + public function setUp() : void + { + $this->schemaWithSDLDirectives = BuildSchema::build(' + directive @onSchema on SCHEMA + directive @onScalar on SCALAR + directive @onObject on OBJECT + directive @onFieldDefinition on FIELD_DEFINITION + directive @onArgumentDefinition on ARGUMENT_DEFINITION + directive @onInterface on INTERFACE + directive @onUnion on UNION + directive @onEnum on ENUM + directive @onEnumValue on ENUM_VALUE + directive @onInputObject on INPUT_OBJECT + directive @onInputFieldDefinition on INPUT_FIELD_DEFINITION + '); + } + + private function expectSDLErrors($sdlString, $schema = null, $errors = []) + { + return $this->expectSDLErrorsFromRule(new KnownDirectives(), $sdlString, $schema, $errors); + } + // Validate: Known directives + /** * @see it('with no directives') */ @@ -113,8 +141,8 @@ public function testWithWellPlacedDirectives() : void $this->expectPassesRule( new KnownDirectives(), ' - query Foo @onQuery { - name @include(if: true) + query Foo($var: Boolean) @onQuery { + name @include(if: $var) ...Frag @include(if: true) skippedField @skip(if: true) ...SkippedFrag @skip(if: true) @@ -127,7 +155,128 @@ public function testWithWellPlacedDirectives() : void ); } - // within schema language + /** + * @see it('with well placed variable definition directive') + */ + public function testWithWellPlacedVariableDefinitionDirective() + { + $this->expectPassesRule( + new KnownDirectives(), + ' + query Foo($var: Boolean @onVariableDefinition) { + name + } + ' + ); + } + + // DESCRIBE: within SDL + + /** + * @see it('with directive defined inside SDL') + */ + public function testWithDirectiveDefinedInsideSDL() + { + $this->expectSDLErrors(' + type Query { + foo: String @test + } + + directive @test on FIELD_DEFINITION + ', null, []); + } + + /** + * @see it('with standard directive') + */ + public function testWithStandardDirective() + { + $this->expectSDLErrors( + ' + type Query { + foo: String @deprecated + }', + null, + [] + ); + } + + /** + * @see it('with overrided standard directive') + */ + public function testWithOverridedStandardDirective() + { + $this->expectSDLErrors( + ' + schema @deprecated { + query: Query + } + directive @deprecated on SCHEMA', + null, + [] + ); + } + + /** + * @see it('with directive defined in schema extension') + */ + public function testWithDirectiveDefinedInSchemaExtension() + { + $schema = BuildSchema::build(' + type Query { + foo: String + } + '); + $this->expectSDLErrors( + ' + directive @test on OBJECT + + extend type Query @test + ', + $schema, + [] + ); + } + + /** + * @see it('with directive used in schema extension') + */ + public function testWithDirectiveUsedInSchemaExtension() + { + $schema = BuildSchema::build(' + directive @test on OBJECT + + type Query { + foo: String + } + '); + $this->expectSDLErrors( + ' + extend type Query @test + ', + $schema, + [] + ); + } + + /** + * @see it('with unknown directive in schema extension') + */ + public function testWithUnknownDirectiveInSchemaExtension() + { + $schema = BuildSchema::build(' + type Query { + foo: String + } + '); + $this->expectSDLErrors( + ' + extend type Query @unknown + ', + $schema, + [$this->unknownDirective('unknown', 2, 29)] + ); + } /** * @see it('with misplaced directives') @@ -137,8 +286,8 @@ public function testWithMisplacedDirectives() : void $this->expectFailsRule( new KnownDirectives(), ' - query Foo @include(if: true) { - name @onQuery + query Foo($var: Boolean) @include(if: true) { + name @onQuery @include(if: $var) ...Frag @onQuery } @@ -147,7 +296,7 @@ public function testWithMisplacedDirectives() : void } ', [ - $this->misplacedDirective('include', 'QUERY', 2, 17), + $this->misplacedDirective('include', 'QUERY', 2, 32), $this->misplacedDirective('onQuery', 'FIELD', 3, 14), $this->misplacedDirective('onQuery', 'FRAGMENT_SPREAD', 4, 17), $this->misplacedDirective('onQuery', 'MUTATION', 7, 20), @@ -155,6 +304,22 @@ public function testWithMisplacedDirectives() : void ); } + /** + * @see it('with misplaced variable definition directive') + */ + public function testWithMisplacedVariableDefinitionDirective() + { + $this->expectFailsRule( + new KnownDirectives(), + ' + query Foo($var: Boolean @onField) { + name + } + ', + [$this->misplacedDirective('onField', 'VARIABLE_DEFINITION', 2, 39)] + ); + } + private function misplacedDirective($directiveName, $placement, $line, $column) { return FormattedError::create( @@ -168,8 +333,7 @@ private function misplacedDirective($directiveName, $placement, $line, $column) */ public function testWSLWithWellPlacedDirectives() : void { - $this->expectPassesRule( - new KnownDirectives(), + $this->expectSDLErrors( ' type MyObj implements MyInterface @onObject { myField(myArg: Int @onArgumentDefinition): String @onFieldDefinition @@ -206,7 +370,9 @@ enum MyEnum @onEnum { schema @onSchema { query: MyQuery } - ' + ', + $this->schemaWithSDLDirectives, + [] ); } @@ -215,8 +381,7 @@ enum MyEnum @onEnum { */ public function testWSLWithMisplacedDirectives() : void { - $this->expectFailsRule( - new KnownDirectives(), + $this->expectSDLErrors( ' type MyObj implements MyInterface @onInterface { myField(myArg: Int @onInputFieldDefinition): String @onInputFieldDefinition @@ -242,6 +407,7 @@ enum MyEnum @onScalar { query: MyQuery } ', + $this->schemaWithSDLDirectives, [ $this->misplacedDirective('onInterface', 'OBJECT', 2, 43), $this->misplacedDirective('onInputFieldDefinition', 'ARGUMENT_DEFINITION', 3, 30), diff --git a/tests/Validator/KnownFragmentNamesTest.php b/tests/Validator/KnownFragmentNamesTest.php index cc9ce862c..5e1f01092 100644 --- a/tests/Validator/KnownFragmentNamesTest.php +++ b/tests/Validator/KnownFragmentNamesTest.php @@ -11,6 +11,7 @@ class KnownFragmentNamesTest extends ValidatorTestCase { // Validate: Known fragment names + /** * @see it('known fragment names are valid') */ diff --git a/tests/Validator/KnownTypeNamesTest.php b/tests/Validator/KnownTypeNamesTest.php index 12834aa35..4c5a142eb 100644 --- a/tests/Validator/KnownTypeNamesTest.php +++ b/tests/Validator/KnownTypeNamesTest.php @@ -11,6 +11,7 @@ class KnownTypeNamesTest extends ValidatorTestCase { // Validate: Known type names + /** * @see it('known type names are valid') */ diff --git a/tests/Validator/LoneAnonymousOperationTest.php b/tests/Validator/LoneAnonymousOperationTest.php index cdd7983b0..5f081fe48 100644 --- a/tests/Validator/LoneAnonymousOperationTest.php +++ b/tests/Validator/LoneAnonymousOperationTest.php @@ -11,6 +11,7 @@ class LoneAnonymousOperationTest extends ValidatorTestCase { // Validate: Anonymous operation must be alone + /** * @see it('no operations') */ diff --git a/tests/Validator/LoneSchemaDefinitionTest.php b/tests/Validator/LoneSchemaDefinitionTest.php new file mode 100644 index 000000000..9e2e517f4 --- /dev/null +++ b/tests/Validator/LoneSchemaDefinitionTest.php @@ -0,0 +1,202 @@ +expectSDLErrorsFromRule(new LoneSchemaDefinition(), $sdlString, $schema, $errors); + } + + private function schemaDefinitionNotAlone($line, $column) + { + return FormattedError::create( + LoneSchemaDefinition::schemaDefinitionNotAloneMessage(), + [new SourceLocation($line, $column)] + ); + } + + private function canNotDefineSchemaWithinExtension($line, $column) + { + return FormattedError::create( + LoneSchemaDefinition::canNotDefineSchemaWithinExtensionMessage(), + [new SourceLocation($line, $column)] + ); + } + + // Validate: Schema definition should be alone + + /** + * @see it('no schema') + */ + public function testNoSchema() + { + $this->expectSDLErrors( + ' + type Query { + foo: String + } + ', + null, + [] + ); + } + + /** + * @see it('one schema definition') + */ + public function testOneSchemaDefinition() + { + $this->expectSDLErrors( + ' + schema { + query: Foo + } + + type Foo { + foo: String + } + ', + null, + [] + ); + } + + /** + * @see it('multiple schema definitions') + */ + public function testMultipleSchemaDefinitions() + { + $this->expectSDLErrors( + ' + schema { + query: Foo + } + + type Foo { + foo: String + } + + schema { + mutation: Foo + } + + schema { + subscription: Foo + } + ', + null, + [ + $this->schemaDefinitionNotAlone(10, 7), + $this->schemaDefinitionNotAlone(14, 7), + ] + ); + } + + /** + * @see it('define schema in schema extension') + */ + public function testDefineSchemaInSchemaExtension() + { + $schema = BuildSchema::build(' + type Foo { + foo: String + } + '); + + $this->expectSDLErrors( + ' + schema { + query: Foo + } + ', + $schema, + [] + ); + } + + /** + * @see it('redefine schema in schema extension') + */ + public function testRedefineSchemaInSchemaExtension() + { + $schema = BuildSchema::build(' + schema { + query: Foo + } + + type Foo { + foo: String + }'); + + $this->expectSDLErrors( + ' + schema { + mutation: Foo + } + ', + $schema, + [$this->canNotDefineSchemaWithinExtension(2, 17)] + ); + } + + /** + * @see it('redefine implicit schema in schema extension') + */ + public function testRedefineImplicitSchemaInSchemaExtension() + { + $schema = BuildSchema::build(' + type Query { + fooField: Foo + } + + type Foo { + foo: String + } + '); + + $this->expectSDLErrors( + ' + schema { + mutation: Foo + } + ', + $schema, + [$this->canNotDefineSchemaWithinExtension(2, 17)] + ); + } + + /** + * @see it('extend schema in schema extension') + */ + public function testExtendSchemaInSchemaExtension() + { + $schema = BuildSchema::build(' + type Query { + fooField: Foo + } + + type Foo { + foo: String + } + '); + + $this->expectSDLErrors( + ' + extend schema { + mutation: Foo + } + ', + $schema, + [] + ); + } +} diff --git a/tests/Validator/NoFragmentCyclesTest.php b/tests/Validator/NoFragmentCyclesTest.php index f0a2a634b..49403d4ef 100644 --- a/tests/Validator/NoFragmentCyclesTest.php +++ b/tests/Validator/NoFragmentCyclesTest.php @@ -11,6 +11,7 @@ class NoFragmentCyclesTest extends ValidatorTestCase { // Validate: No circular fragment spreads + /** * @see it('single reference is valid') */ diff --git a/tests/Validator/NoUndefinedVariablesTest.php b/tests/Validator/NoUndefinedVariablesTest.php index ea2e25e1f..318e9da52 100644 --- a/tests/Validator/NoUndefinedVariablesTest.php +++ b/tests/Validator/NoUndefinedVariablesTest.php @@ -11,6 +11,7 @@ class NoUndefinedVariablesTest extends ValidatorTestCase { // Validate: No undefined variables + /** * @see it('all variables defined') */ diff --git a/tests/Validator/NoUnusedFragmentsTest.php b/tests/Validator/NoUnusedFragmentsTest.php index 9d794a818..eccc6b193 100644 --- a/tests/Validator/NoUnusedFragmentsTest.php +++ b/tests/Validator/NoUnusedFragmentsTest.php @@ -11,6 +11,7 @@ class NoUnusedFragmentsTest extends ValidatorTestCase { // Validate: No unused fragments + /** * @see it('all fragment names are used') */ diff --git a/tests/Validator/NoUnusedVariablesTest.php b/tests/Validator/NoUnusedVariablesTest.php index 7dc7d401b..846ecd2ec 100644 --- a/tests/Validator/NoUnusedVariablesTest.php +++ b/tests/Validator/NoUnusedVariablesTest.php @@ -11,6 +11,7 @@ class NoUnusedVariablesTest extends ValidatorTestCase { // Validate: No unused variables + /** * @see it('uses all variables') */ diff --git a/tests/Validator/OverlappingFieldsCanBeMergedTest.php b/tests/Validator/OverlappingFieldsCanBeMergedTest.php index 30291fa7d..9b3879431 100644 --- a/tests/Validator/OverlappingFieldsCanBeMergedTest.php +++ b/tests/Validator/OverlappingFieldsCanBeMergedTest.php @@ -15,6 +15,7 @@ class OverlappingFieldsCanBeMergedTest extends ValidatorTestCase { // Validate: Overlapping fields can be merged + /** * @see it('unique fields') */ @@ -703,7 +704,7 @@ private function getSchema() $SomeBox = new InterfaceType([ 'name' => 'SomeBox', - 'fields' => static function () use (&$SomeBox) { + 'fields' => static function () use (&$SomeBox) : array { return [ 'deepBox' => ['type' => $SomeBox], 'unrelatedField' => ['type' => Type::string()], @@ -714,7 +715,7 @@ private function getSchema() $StringBox = new ObjectType([ 'name' => 'StringBox', 'interfaces' => [$SomeBox], - 'fields' => static function () use (&$StringBox, &$IntBox) { + 'fields' => static function () use (&$StringBox, &$IntBox) : array { return [ 'scalar' => ['type' => Type::string()], 'deepBox' => ['type' => $StringBox], @@ -729,7 +730,7 @@ private function getSchema() $IntBox = new ObjectType([ 'name' => 'IntBox', 'interfaces' => [$SomeBox], - 'fields' => static function () use (&$StringBox, &$IntBox) { + 'fields' => static function () use (&$StringBox, &$IntBox) : array { return [ 'scalar' => ['type' => Type::int()], 'deepBox' => ['type' => $IntBox], diff --git a/tests/Validator/PossibleFragmentSpreadsTest.php b/tests/Validator/PossibleFragmentSpreadsTest.php index c4a8b293b..a8f6f8514 100644 --- a/tests/Validator/PossibleFragmentSpreadsTest.php +++ b/tests/Validator/PossibleFragmentSpreadsTest.php @@ -11,6 +11,7 @@ class PossibleFragmentSpreadsTest extends ValidatorTestCase { // Validate: Possible fragment spreads + /** * @see it('of the same object') */ diff --git a/tests/Validator/ProvidedNonNullArgumentsTest.php b/tests/Validator/ProvidedRequiredArgumentsTest.php similarity index 57% rename from tests/Validator/ProvidedNonNullArgumentsTest.php rename to tests/Validator/ProvidedRequiredArgumentsTest.php index 803fcda59..bb0c09e3d 100644 --- a/tests/Validator/ProvidedNonNullArgumentsTest.php +++ b/tests/Validator/ProvidedRequiredArgumentsTest.php @@ -6,11 +6,14 @@ use GraphQL\Error\FormattedError; use GraphQL\Language\SourceLocation; -use GraphQL\Validator\Rules\ProvidedNonNullArguments; +use GraphQL\Utils\BuildSchema; +use GraphQL\Validator\Rules\ProvidedRequiredArguments; +use GraphQL\Validator\Rules\ProvidedRequiredArgumentsOnDirectives; -class ProvidedNonNullArgumentsTest extends ValidatorTestCase +class ProvidedRequiredArgumentsTest extends ValidatorTestCase { // Validate: Provided required arguments + /** * @see it('ignores unknown arguments') */ @@ -18,7 +21,7 @@ public function testIgnoresUnknownArguments() : void { // ignores unknown arguments $this->expectPassesRule( - new ProvidedNonNullArguments(), + new ProvidedRequiredArguments(), ' { dog { @@ -37,7 +40,7 @@ public function testIgnoresUnknownArguments() : void public function testArgOnOptionalArg() : void { $this->expectPassesRule( - new ProvidedNonNullArguments(), + new ProvidedRequiredArguments(), ' { dog { @@ -54,7 +57,7 @@ public function testArgOnOptionalArg() : void public function testNoArgOnOptionalArg() : void { $this->expectPassesRule( - new ProvidedNonNullArguments(), + new ProvidedRequiredArguments(), ' { dog { @@ -65,13 +68,30 @@ public function testNoArgOnOptionalArg() : void ); } + /** + * @see it('No arg on non-null field with default') + */ + public function testNoArgOnNonNullFieldWithDefault() + { + $this->expectPassesRule( + new ProvidedRequiredArguments(), + ' + { + complicatedArgs { + nonNullFieldWithDefault + } + } + ' + ); + } + /** * @see it('Multiple args') */ public function testMultipleArgs() : void { $this->expectPassesRule( - new ProvidedNonNullArguments(), + new ProvidedRequiredArguments(), ' { complicatedArgs { @@ -88,7 +108,7 @@ public function testMultipleArgs() : void public function testMultipleArgsReverseOrder() : void { $this->expectPassesRule( - new ProvidedNonNullArguments(), + new ProvidedRequiredArguments(), ' { complicatedArgs { @@ -105,7 +125,7 @@ public function testMultipleArgsReverseOrder() : void public function testNoArgsOnMultipleOptional() : void { $this->expectPassesRule( - new ProvidedNonNullArguments(), + new ProvidedRequiredArguments(), ' { complicatedArgs { @@ -122,7 +142,7 @@ public function testNoArgsOnMultipleOptional() : void public function testOneArgOnMultipleOptional() : void { $this->expectPassesRule( - new ProvidedNonNullArguments(), + new ProvidedRequiredArguments(), ' { complicatedArgs { @@ -139,7 +159,7 @@ public function testOneArgOnMultipleOptional() : void public function testSecondArgOnMultipleOptional() : void { $this->expectPassesRule( - new ProvidedNonNullArguments(), + new ProvidedRequiredArguments(), ' { complicatedArgs { @@ -156,7 +176,7 @@ public function testSecondArgOnMultipleOptional() : void public function testMultipleReqsOnMixedList() : void { $this->expectPassesRule( - new ProvidedNonNullArguments(), + new ProvidedRequiredArguments(), ' { complicatedArgs { @@ -173,7 +193,7 @@ public function testMultipleReqsOnMixedList() : void public function testMultipleReqsAndOneOptOnMixedList() : void { $this->expectPassesRule( - new ProvidedNonNullArguments(), + new ProvidedRequiredArguments(), ' { complicatedArgs { @@ -190,7 +210,7 @@ public function testMultipleReqsAndOneOptOnMixedList() : void public function testAllReqsAndOptsOnMixedList() : void { $this->expectPassesRule( - new ProvidedNonNullArguments(), + new ProvidedRequiredArguments(), ' { complicatedArgs { @@ -209,7 +229,7 @@ public function testAllReqsAndOptsOnMixedList() : void public function testMissingOneNonNullableArgument() : void { $this->expectFailsRule( - new ProvidedNonNullArguments(), + new ProvidedRequiredArguments(), ' { complicatedArgs { @@ -224,7 +244,7 @@ public function testMissingOneNonNullableArgument() : void private function missingFieldArg($fieldName, $argName, $typeName, $line, $column) { return FormattedError::create( - ProvidedNonNullArguments::missingFieldArgMessage($fieldName, $argName, $typeName), + ProvidedRequiredArguments::missingFieldArgMessage($fieldName, $argName, $typeName), [new SourceLocation($line, $column)] ); } @@ -235,7 +255,7 @@ private function missingFieldArg($fieldName, $argName, $typeName, $line, $column public function testMissingMultipleNonNullableArguments() : void { $this->expectFailsRule( - new ProvidedNonNullArguments(), + new ProvidedRequiredArguments(), ' { complicatedArgs { @@ -258,7 +278,7 @@ public function testMissingMultipleNonNullableArguments() : void public function testIncorrectValueAndMissingArgument() : void { $this->expectFailsRule( - new ProvidedNonNullArguments(), + new ProvidedRequiredArguments(), ' { complicatedArgs { @@ -278,7 +298,7 @@ public function testIncorrectValueAndMissingArgument() : void public function testIgnoresUnknownDirectives() : void { $this->expectPassesRule( - new ProvidedNonNullArguments(), + new ProvidedRequiredArguments(), ' { dog @unknown @@ -293,7 +313,7 @@ public function testIgnoresUnknownDirectives() : void public function testWithDirectivesOfValidTypes() : void { $this->expectPassesRule( - new ProvidedNonNullArguments(), + new ProvidedRequiredArguments(), ' { dog @include(if: true) { @@ -313,7 +333,7 @@ public function testWithDirectivesOfValidTypes() : void public function testWithDirectiveWithMissingTypes() : void { $this->expectFailsRule( - new ProvidedNonNullArguments(), + new ProvidedRequiredArguments(), ' { dog @include { @@ -328,10 +348,124 @@ public function testWithDirectiveWithMissingTypes() : void ); } + // Describe: within SDL + + /** + * @see it('Missing optional args on directive defined inside SDL') + */ + public function testMissingOptionalArgsOnDirectiveDefinedInsideSDL() + { + $this->expectPassesRule( + new ProvidedRequiredArgumentsOnDirectives(), + ' + type Query { + foo: String @test + } + + directive @test(arg1: String, arg2: String! = "") on FIELD_DEFINITION + ' + ); + } + + /** + * @see it('Missing arg on directive defined inside SDL') + */ + public function testMissingArgOnDirectiveDefinedInsideSDL() + { + $this->expectFailsRule( + new ProvidedRequiredArgumentsOnDirectives(), + ' + type Query { + foo: String @test + } + + directive @test(arg: String!) on FIELD_DEFINITION + ', + [$this->missingDirectiveArg('test', 'arg', 'String!', 3, 31)] + ); + } + + /** + * @see it('Missing arg on standard directive') + */ + public function testMissingArgOnStandardDirective() + { + $this->expectFailsRule( + new ProvidedRequiredArgumentsOnDirectives(), + ' + type Query { + foo: String @include + } + ', + [$this->missingDirectiveArg('include', 'if', 'Boolean!', 3, 31)] + ); + } + + /** + * @see it('Missing arg on overrided standard directive') + */ + public function testMissingArgOnOverridedStandardDirective() + { + $this->expectFailsRule( + new ProvidedRequiredArgumentsOnDirectives(), + ' + type Query { + foo: String @deprecated + } + directive @deprecated(reason: String!) on FIELD + ', + [$this->missingDirectiveArg('deprecated', 'reason', 'String!', 3, 31)] + ); + } + + /** + * @see it('Missing arg on directive defined in schema extension') + */ + public function testMissingArgOnDirectiveDefinedInSchemaExtension() + { + $schema = BuildSchema::build(' + type Query { + foo: String + } + '); + $this->expectInvalid( + $schema, + [new ProvidedRequiredArgumentsOnDirectives()], + ' + directive @test(arg: String!) on OBJECT + + extend type Query @test + ', + [$this->missingDirectiveArg('test', 'arg', 'String!', 4, 36)] + ); + } + + /** + * @see it('Missing arg on directive used in schema extension') + */ + public function testMissingArgOnDirectiveUsedInSchemaExtension() + { + $schema = BuildSchema::build(' + directive @test(arg: String!) on OBJECT + + type Query { + foo: String + } + '); + $this->expectInvalid( + $schema, + [new ProvidedRequiredArgumentsOnDirectives()], + ' + extend type Query @test + ', + [$this->missingDirectiveArg('test', 'arg', 'String!', 2, 35)] + ); + } + private function missingDirectiveArg($directiveName, $argName, $typeName, $line, $column) { return FormattedError::create( - ProvidedNonNullArguments::missingDirectiveArgMessage($directiveName, $argName, $typeName), + ProvidedRequiredArgumentsOnDirectives::missingDirectiveArgMessage($directiveName, $argName, $typeName), [new SourceLocation($line, $column)] ); } diff --git a/tests/Validator/QueryComplexityTest.php b/tests/Validator/QueryComplexityTest.php index 998b751ff..0a14fb966 100644 --- a/tests/Validator/QueryComplexityTest.php +++ b/tests/Validator/QueryComplexityTest.php @@ -25,6 +25,21 @@ public function testSimpleQueries() : void $this->assertDocumentValidators($query, 2, 3); } + public function testGetQueryComplexity() : void + { + $query = 'query MyQuery { human { firstName } }'; + + $rule = $this->getRule(5); + + DocumentValidator::validate( + QuerySecuritySchema::buildSchema(), + Parser::parse($query), + [$rule] + ); + + self::assertEquals(2, $rule->getQueryComplexity(), $query); + } + private function assertDocumentValidators($query, $queryComplexity, $startComplexity) { for ($maxComplexity = $startComplexity; $maxComplexity >= 0; --$maxComplexity) { @@ -146,6 +161,22 @@ public function testQueryWithMultipleDirectives() : void $this->assertDocumentValidators($query, 2, 3); } + public function testQueryWithCustomDirective() : void + { + $query = 'query MyQuery { human { ... on Human { firstName @foo(bar: false) } } }'; + + $this->assertDocumentValidators($query, 2, 3); + } + + public function testQueryWithCustomAndSkipDirective() : void + { + $query = 'query MyQuery($withoutDogs: Boolean!) { human { dogs(name: "Root") @skip(if:$withoutDogs) { name @foo(bar: true) } } }'; + + $this->getRule()->setRawVariableValues(['withoutDogs' => true]); + + $this->assertDocumentValidators($query, 1, 2); + } + public function testComplexityIntrospectionQuery() : void { $this->assertIntrospectionQuery(181); @@ -168,10 +199,10 @@ public function testSkippedWhenThereAreOtherValidationErrors() : void $reportedError = new Error('OtherValidatorError'); $otherRule = new CustomValidationRule( 'otherRule', - static function (ValidationContext $context) use ($reportedError) { + static function (ValidationContext $context) use ($reportedError) : array { return [ NodeKind::OPERATION_DEFINITION => [ - 'leave' => static function () use ($context, $reportedError) { + 'leave' => static function () use ($context, $reportedError) : void { $context->reportError($reportedError); }, ], diff --git a/tests/Validator/QuerySecuritySchema.php b/tests/Validator/QuerySecuritySchema.php index 64fcf4558..58ba2692e 100644 --- a/tests/Validator/QuerySecuritySchema.php +++ b/tests/Validator/QuerySecuritySchema.php @@ -4,15 +4,23 @@ namespace GraphQL\Tests\Validator; +use GraphQL\GraphQL; +use GraphQL\Language\DirectiveLocation; +use GraphQL\Type\Definition\Directive; +use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Schema; +use function array_merge; class QuerySecuritySchema { /** @var Schema */ private static $schema; + /** @var Directive */ + private static $fooDirective; + /** @var ObjectType */ private static $dogType; @@ -32,7 +40,8 @@ public static function buildSchema() } self::$schema = new Schema([ - 'query' => static::buildQueryRootType(), + 'query' => static::buildQueryRootType(), + 'directives' => array_merge(GraphQL::getStandardDirectives(), [static::buildFooDirective()]), ]); return self::$schema; @@ -110,4 +119,24 @@ public static function buildDogType() return self::$dogType; } + + public static function buildFooDirective() : Directive + { + if (self::$fooDirective !== null) { + return self::$fooDirective; + } + + self::$fooDirective = new Directive([ + 'name' => 'foo', + 'locations' => [DirectiveLocation::FIELD], + 'args' => [new FieldArgument([ + 'name' => 'bar', + 'type' => Type::nonNull(Type::boolean()), + 'defaultValue' => ' ', + ]), + ], + ]); + + return self::$fooDirective; + } } diff --git a/tests/Validator/QuerySecurityTestCase.php b/tests/Validator/QuerySecurityTestCase.php index 4eb5f74ab..8661f01cb 100644 --- a/tests/Validator/QuerySecurityTestCase.php +++ b/tests/Validator/QuerySecurityTestCase.php @@ -10,17 +10,17 @@ use GraphQL\Type\Introspection; use GraphQL\Validator\DocumentValidator; use GraphQL\Validator\Rules\QuerySecurityRule; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; use function array_map; abstract class QuerySecurityTestCase extends TestCase { - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage argument must be greater or equal to 0. - */ public function testMaxQueryDepthMustBeGreaterOrEqualTo0() : void { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('argument must be greater or equal to 0.'); + $this->getRule(-1); } diff --git a/tests/Validator/ScalarLeafsTest.php b/tests/Validator/ScalarLeafsTest.php index c787be320..d7f2c54a2 100644 --- a/tests/Validator/ScalarLeafsTest.php +++ b/tests/Validator/ScalarLeafsTest.php @@ -11,6 +11,7 @@ class ScalarLeafsTest extends ValidatorTestCase { // Validate: Scalar leafs + /** * @see it('valid scalar selection') */ diff --git a/tests/Validator/SingleFieldSubscriptionsTest.php b/tests/Validator/SingleFieldSubscriptionsTest.php new file mode 100644 index 000000000..245f311da --- /dev/null +++ b/tests/Validator/SingleFieldSubscriptionsTest.php @@ -0,0 +1,193 @@ +expectPassesRule( + new SingleFieldSubscription(), + ' + subscription sub { + catSubscribe { + meows + } + } + ' + ); + } + + /** + * @see it('valid single field bulk subscriptions') + */ + public function testValidSingleFieldBulkSubscriptions() : void + { + $this->expectPassesRule( + new SingleFieldSubscription(), + ' + subscription sub { + catSubscribe { + meows + } + } + + subscription sub2 { + dogSubscribe { + barks + } + } + ' + ); + } + + /** + * @see it('valid single field anonymous subscription') + */ + public function testValidSingleFieldAnonymousSubscription() : void + { + $this->expectPassesRule( + new SingleFieldSubscription(), + ' + subscription { + catSubscribe { + meows + } + } + ' + ); + } + + /** + * @see it('valid single field subscription') + */ + public function testValidSingleFieldSubscriptionWithMultipleResultFields() : void + { + $this->expectPassesRule( + new SingleFieldSubscription(), + ' + subscription { + catSubscribe { + meows + meowVolume + } + } + ' + ); + } + + /** + * @see it('invalid multiple field subscription') + */ + public function testInvalidMultipleFieldSubscription() : void + { + $this->expectFailsRule( + new SingleFieldSubscription(), + ' + subscription sub { + catSubscribe { + meows + } + barkSubscribe { + barks + } + } + ', + [$this->multipleFieldsInOperation('sub', [6, 9])] + ); + } + + /** + * @see it('invalid multiple field anonymous subscription') + */ + public function testInvalidMultipleFieldAnonymousSubscription() : void + { + $this->expectFailsRule( + new SingleFieldSubscription(), + ' + subscription { + catSubscribe { + meows + } + barkSubscribe { + barks + } + } + ', + [$this->multipleFieldsInOperation(null, [6, 9])] + ); + } + + /** + * @see it('invalid many fields subscription') + */ + public function testInvalidManyFieldsSubscription() : void + { + $this->expectFailsRule( + new SingleFieldSubscription(), + ' + subscription sub { + first: catSubscribe { + meows + } + second: catSubscribe { + meows + } + third: catSubscribe { + meows + } + } + ', + [$this->multipleFieldsInOperation('sub', [6, 9], [9, 9])] + ); + } + + /** + * @see it('invalid many fields anonymous subscription') + */ + public function testInvalidManyFieldAnonymousSubscription() : void + { + $this->expectFailsRule( + new SingleFieldSubscription(), + ' + subscription { + first: catSubscribe { + meows + } + second: catSubscribe { + meows + } + third: catSubscribe { + meows + } + } + ', + [$this->multipleFieldsInOperation(null, [6, 9], [9, 9])] + ); + } + + /** + * @param array ...$locations A tuple of line and column + */ + private function multipleFieldsInOperation(?string $operationName, array ...$locations) + { + return FormattedError::create( + SingleFieldSubscription::multipleFieldsInOperation($operationName), + array_map(static function (array $location) : SourceLocation { + [$line, $column] = $location; + + return new SourceLocation($line, $column); + }, $locations) + ); + } +} diff --git a/tests/Validator/UniqueArgumentNamesTest.php b/tests/Validator/UniqueArgumentNamesTest.php index 44b0ac99d..3629ab856 100644 --- a/tests/Validator/UniqueArgumentNamesTest.php +++ b/tests/Validator/UniqueArgumentNamesTest.php @@ -11,6 +11,7 @@ class UniqueArgumentNamesTest extends ValidatorTestCase { // Validate: Unique argument names + /** * @see it('no arguments on field') */ diff --git a/tests/Validator/UniqueDirectivesPerLocationTest.php b/tests/Validator/UniqueDirectivesPerLocationTest.php index e1852201e..5e7d656c6 100644 --- a/tests/Validator/UniqueDirectivesPerLocationTest.php +++ b/tests/Validator/UniqueDirectivesPerLocationTest.php @@ -8,6 +8,11 @@ class UniqueDirectivesPerLocationTest extends ValidatorTestCase { + private function expectSDLErrors($sdlString, $schema = null, $errors = []) + { + $this->expectSDLErrorsFromRule(new UniqueDirectivesPerLocation(), $sdlString, $schema, $errors); + } + /** * @see it('no directives') */ @@ -84,6 +89,39 @@ public function testSameDirectivesInSimilarLocations() : void ); } + /** + * @see it('repeatable directives in same location', () => { + */ + public function testRepeatableDirectivesInSameLocation() : void + { + $this->expectPassesRule( + new UniqueDirectivesPerLocation(), + ' + fragment Test on Type @repeatable @repeatable { + field @repeatable @repeatable + } + ' + ); + } + + /** + * @see it('unknown directives must be ignored', () => { + */ + public function testUnknownDirectivesMustBeIgnored() : void + { + $this->expectPassesRule( + new UniqueDirectivesPerLocation(), + ' + type Test @unknown @unknown { + field: String! @unknown @unknown + } + extend type Test @unknown { + anotherField: String! + } + ' + ); + } + /** * @see it('duplicate directives in one location') */ @@ -167,4 +205,34 @@ public function testDuplicateDirectivesInManyLocations() : void ] ); } + + /** + * @see it('duplicate directives on SDL definitions') + */ + public function testDuplicateDirectivesOnSDLDefinitions() + { + $this->expectSDLErrors( + ' + directive @nonRepeatable on + SCHEMA | SCALAR | OBJECT | INTERFACE | UNION | INPUT_OBJECT + + schema @nonRepeatable @nonRepeatable { query: Dummy } + + scalar TestScalar @nonRepeatable @nonRepeatable + type TestObject @nonRepeatable @nonRepeatable + interface TestInterface @nonRepeatable @nonRepeatable + union TestUnion @nonRepeatable @nonRepeatable + input TestInput @nonRepeatable @nonRepeatable + ', + null, + [ + $this->duplicateDirective('nonRepeatable', 5, 14, 5, 29), + $this->duplicateDirective('nonRepeatable', 7, 25, 7, 40), + $this->duplicateDirective('nonRepeatable', 8, 23, 8, 38), + $this->duplicateDirective('nonRepeatable', 9, 31, 9, 46), + $this->duplicateDirective('nonRepeatable', 10, 23, 10, 38), + $this->duplicateDirective('nonRepeatable', 11, 23, 11, 38), + ] + ); + } } diff --git a/tests/Validator/UniqueFragmentNamesTest.php b/tests/Validator/UniqueFragmentNamesTest.php index b34b76f7b..abec62168 100644 --- a/tests/Validator/UniqueFragmentNamesTest.php +++ b/tests/Validator/UniqueFragmentNamesTest.php @@ -11,6 +11,7 @@ class UniqueFragmentNamesTest extends ValidatorTestCase { // Validate: Unique fragment names + /** * @see it('no fragments') */ diff --git a/tests/Validator/UniqueInputFieldNamesTest.php b/tests/Validator/UniqueInputFieldNamesTest.php index 81c35a025..985441589 100644 --- a/tests/Validator/UniqueInputFieldNamesTest.php +++ b/tests/Validator/UniqueInputFieldNamesTest.php @@ -11,6 +11,7 @@ class UniqueInputFieldNamesTest extends ValidatorTestCase { // Validate: Unique input field names + /** * @see it('input object with fields') */ diff --git a/tests/Validator/UniqueOperationNamesTest.php b/tests/Validator/UniqueOperationNamesTest.php index 3bc90b5a4..c8083247f 100644 --- a/tests/Validator/UniqueOperationNamesTest.php +++ b/tests/Validator/UniqueOperationNamesTest.php @@ -11,6 +11,7 @@ class UniqueOperationNamesTest extends ValidatorTestCase { // Validate: Unique operation names + /** * @see it('no operations') */ diff --git a/tests/Validator/UniqueVariableNamesTest.php b/tests/Validator/UniqueVariableNamesTest.php index f587efb38..b4dc60906 100644 --- a/tests/Validator/UniqueVariableNamesTest.php +++ b/tests/Validator/UniqueVariableNamesTest.php @@ -11,6 +11,7 @@ class UniqueVariableNamesTest extends ValidatorTestCase { // Validate: Unique variable names + /** * @see it('unique variable names') */ diff --git a/tests/Validator/ValidationTest.php b/tests/Validator/ValidationTest.php index e823cdc8d..428c342a5 100644 --- a/tests/Validator/ValidationTest.php +++ b/tests/Validator/ValidationTest.php @@ -7,6 +7,7 @@ class ValidationTest extends ValidatorTestCase { // Validate: Supports full validation + /** * @see it('validates queries') */ diff --git a/tests/Validator/ValidatorTestCase.php b/tests/Validator/ValidatorTestCase.php index 55e913666..12b6a9240 100644 --- a/tests/Validator/ValidatorTestCase.php +++ b/tests/Validator/ValidatorTestCase.php @@ -5,6 +5,7 @@ namespace GraphQL\Tests\Validator; use Exception; +use GraphQL\Language\DirectiveLocation; use GraphQL\Language\Parser; use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\Directive; @@ -21,16 +22,16 @@ abstract class ValidatorTestCase extends TestCase { - protected function expectPassesRule($rule, $queryString) : void + protected function expectPassesRule($rule, $queryString, $options = []) : void { - $this->expectValid(self::getTestSchema(), [$rule], $queryString); + $this->expectValid(self::getTestSchema(), [$rule], $queryString, $options); } - protected function expectValid($schema, $rules, $queryString) : void + protected function expectValid($schema, $rules, $queryString, $options = []) : void { self::assertEquals( [], - DocumentValidator::validate($schema, Parser::parse($queryString), $rules), + DocumentValidator::validate($schema, Parser::parse($queryString, $options), $rules), 'Should validate' ); } @@ -64,7 +65,7 @@ public static function getTestSchema() $Canine = new InterfaceType([ 'name' => 'Canine', - 'fields' => static function () { + 'fields' => static function () : array { return [ 'name' => [ 'type' => Type::string(), @@ -111,7 +112,7 @@ public static function getTestSchema() $Cat = new ObjectType([ 'name' => 'Cat', - 'fields' => static function () use (&$FurColor) { + 'fields' => static function () use (&$FurColor) : array { return [ 'name' => [ 'type' => Type::string(), @@ -142,7 +143,7 @@ public static function getTestSchema() $Human = new ObjectType([ 'name' => 'Human', 'interfaces' => [$Being, $Intelligent], - 'fields' => static function () use (&$Human, $Pet) { + 'fields' => static function () use (&$Human, $Pet) : array { return [ 'name' => [ 'type' => Type::string(), @@ -193,6 +194,7 @@ public static function getTestSchema() 'name' => 'ComplexInput', 'fields' => [ 'requiredField' => ['type' => Type::nonNull(Type::boolean())], + 'nonNullField' => ['type' => Type::nonNull(Type::boolean()), 'defaultValue' => false], 'intField' => ['type' => Type::int()], 'stringField' => ['type' => Type::string()], 'booleanField' => ['type' => Type::boolean()], @@ -257,6 +259,12 @@ public static function getTestSchema() 'req2' => ['type' => Type::nonNull(Type::int())], ], ], + 'nonNullFieldWithDefault' => [ + 'type' => Type::string(), + 'args' => [ + 'arg' => [ 'type' => Type::nonNull(Type::int()), 'defaultValue' => 0 ], + ], + ], 'multipleOpts' => [ 'type' => Type::string(), 'args' => [ @@ -293,10 +301,10 @@ public static function getTestSchema() 'serialize' => static function ($value) { return $value; }, - 'parseLiteral' => static function ($node) { + 'parseLiteral' => static function ($node) : void { throw new Exception('Invalid scalar is always invalid: ' . $node->value); }, - 'parseValue' => static function ($node) { + 'parseValue' => static function ($node) : void { throw new Exception('Invalid scalar is always invalid: ' . $node); }, ]); @@ -342,95 +350,77 @@ public static function getTestSchema() ], ]); + $subscriptionRoot = new ObjectType([ + 'name' => 'SubscriptionRoot', + 'fields' => [ + 'catSubscribe' => ['type' => $Cat], + 'barkSubscribe' => ['type' => $Dog], + ], + ]); + return new Schema([ - 'query' => $queryRoot, - 'directives' => [ + 'query' => $queryRoot, + 'subscription' => $subscriptionRoot, + 'directives' => [ Directive::includeDirective(), Directive::skipDirective(), + Directive::deprecatedDirective(), + new Directive([ + 'name' => 'directive', + 'locations' => [DirectiveLocation::FIELD], + ]), + new Directive([ + 'name' => 'directiveA', + 'locations' => [DirectiveLocation::FIELD], + ]), + new Directive([ + 'name' => 'directiveB', + 'locations' => [DirectiveLocation::FIELD], + ]), new Directive([ 'name' => 'onQuery', - 'locations' => ['QUERY'], + 'locations' => [DirectiveLocation::QUERY], ]), new Directive([ 'name' => 'onMutation', - 'locations' => ['MUTATION'], + 'locations' => [DirectiveLocation::MUTATION], ]), new Directive([ 'name' => 'onSubscription', - 'locations' => ['SUBSCRIPTION'], + 'locations' => [DirectiveLocation::SUBSCRIPTION], ]), new Directive([ 'name' => 'onField', - 'locations' => ['FIELD'], + 'locations' => [DirectiveLocation::FIELD], ]), new Directive([ 'name' => 'onFragmentDefinition', - 'locations' => ['FRAGMENT_DEFINITION'], + 'locations' => [DirectiveLocation::FRAGMENT_DEFINITION], ]), new Directive([ 'name' => 'onFragmentSpread', - 'locations' => ['FRAGMENT_SPREAD'], + 'locations' => [DirectiveLocation::FRAGMENT_SPREAD], ]), new Directive([ 'name' => 'onInlineFragment', - 'locations' => ['INLINE_FRAGMENT'], - ]), - new Directive([ - 'name' => 'onSchema', - 'locations' => ['SCHEMA'], - ]), - new Directive([ - 'name' => 'onScalar', - 'locations' => ['SCALAR'], - ]), - new Directive([ - 'name' => 'onObject', - 'locations' => ['OBJECT'], - ]), - new Directive([ - 'name' => 'onFieldDefinition', - 'locations' => ['FIELD_DEFINITION'], - ]), - new Directive([ - 'name' => 'onArgumentDefinition', - 'locations' => ['ARGUMENT_DEFINITION'], - ]), - new Directive([ - 'name' => 'onInterface', - 'locations' => ['INTERFACE'], - ]), - new Directive([ - 'name' => 'onUnion', - 'locations' => ['UNION'], - ]), - new Directive([ - 'name' => 'onEnum', - 'locations' => ['ENUM'], - ]), - new Directive([ - 'name' => 'onEnumValue', - 'locations' => ['ENUM_VALUE'], - ]), - new Directive([ - 'name' => 'onInputObject', - 'locations' => ['INPUT_OBJECT'], + 'locations' => [DirectiveLocation::INLINE_FRAGMENT], ]), new Directive([ - 'name' => 'onInputFieldDefinition', - 'locations' => ['INPUT_FIELD_DEFINITION'], + 'name' => 'onVariableDefinition', + 'locations' => [DirectiveLocation::VARIABLE_DEFINITION], ]), ], ]); } - protected function expectFailsRule($rule, $queryString, $errors) + protected function expectFailsRule($rule, $queryString, $errors, $options = []) { - return $this->expectInvalid(self::getTestSchema(), [$rule], $queryString, $errors); + return $this->expectInvalid(self::getTestSchema(), [$rule], $queryString, $errors, $options); } - protected function expectInvalid($schema, $rules, $queryString, $expectedErrors) + protected function expectInvalid($schema, $rules, $queryString, $expectedErrors, $options = []) { - $errors = DocumentValidator::validate($schema, Parser::parse($queryString), $rules); + $errors = DocumentValidator::validate($schema, Parser::parse($queryString, $options), $rules); self::assertNotEmpty($errors, 'GraphQL should not validate'); self::assertEquals($expectedErrors, array_map(['GraphQL\Error\Error', 'formatError'], $errors)); @@ -457,4 +447,13 @@ protected function expectFailsCompleteValidation($queryString, $errors) : void { $this->expectInvalid(self::getTestSchema(), DocumentValidator::allRules(), $queryString, $errors); } + + protected function expectSDLErrorsFromRule($rule, $sdlString, ?Schema $schema = null, $errors = []) + { + $actualErrors = DocumentValidator::validateSDL(Parser::parse($sdlString), $schema, [$rule]); + self::assertEquals( + $errors, + array_map(['GraphQL\Error\Error', 'formatError'], $actualErrors) + ); + } } diff --git a/tests/Validator/ValuesOfCorrectTypeTest.php b/tests/Validator/ValuesOfCorrectTypeTest.php index ac927084e..35487dfe8 100644 --- a/tests/Validator/ValuesOfCorrectTypeTest.php +++ b/tests/Validator/ValuesOfCorrectTypeTest.php @@ -230,7 +230,7 @@ public function testNullIntoNullableType() : void */ public function testIntIntoString() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -243,6 +243,8 @@ public function testIntIntoString() : void $this->badValueWithMessage('Field "stringArgField" argument "stringArg" requires type String, found 1.', 4, 39), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } private function badValue($typeName, $value, $line, $column, $message = null) @@ -267,7 +269,7 @@ private function badValueWithMessage($message, $line, $column) */ public function testFloatIntoString() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -280,6 +282,8 @@ public function testFloatIntoString() : void $this->badValueWithMessage('Field "stringArgField" argument "stringArg" requires type String, found 1.0.', 4, 39), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } // Invalid String values @@ -289,7 +293,7 @@ public function testFloatIntoString() : void */ public function testBooleanIntoString() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -302,6 +306,8 @@ public function testBooleanIntoString() : void $this->badValueWithMessage('Field "stringArgField" argument "stringArg" requires type String, found true.', 4, 39), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -309,7 +315,7 @@ public function testBooleanIntoString() : void */ public function testUnquotedStringIntoString() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -322,6 +328,8 @@ public function testUnquotedStringIntoString() : void $this->badValueWithMessage('Field "stringArgField" argument "stringArg" requires type String, found BAR.', 4, 39), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -329,7 +337,7 @@ public function testUnquotedStringIntoString() : void */ public function testStringIntoInt() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -342,6 +350,8 @@ public function testStringIntoInt() : void $this->badValueWithMessage('Field "intArgField" argument "intArg" requires type Int, found "3".', 4, 33), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -349,7 +359,7 @@ public function testStringIntoInt() : void */ public function testBigIntIntoInt() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -362,6 +372,8 @@ public function testBigIntIntoInt() : void $this->badValueWithMessage('Field "intArgField" argument "intArg" requires type Int, found 829384293849283498239482938.', 4, 33), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } // Invalid Int values @@ -371,7 +383,7 @@ public function testBigIntIntoInt() : void */ public function testUnquotedStringIntoInt() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -384,6 +396,8 @@ public function testUnquotedStringIntoInt() : void $this->badValueWithMessage('Field "intArgField" argument "intArg" requires type Int, found FOO.', 4, 33), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -391,7 +405,7 @@ public function testUnquotedStringIntoInt() : void */ public function testSimpleFloatIntoInt() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -404,6 +418,8 @@ public function testSimpleFloatIntoInt() : void $this->badValueWithMessage('Field "intArgField" argument "intArg" requires type Int, found 3.0.', 4, 33), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -411,7 +427,7 @@ public function testSimpleFloatIntoInt() : void */ public function testFloatIntoInt() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -424,6 +440,8 @@ public function testFloatIntoInt() : void $this->badValueWithMessage('Field "intArgField" argument "intArg" requires type Int, found 3.333.', 4, 33), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -431,7 +449,7 @@ public function testFloatIntoInt() : void */ public function testStringIntoFloat() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -444,6 +462,8 @@ public function testStringIntoFloat() : void $this->badValueWithMessage('Field "floatArgField" argument "floatArg" requires type Float, found "3.333".', 4, 37), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -451,7 +471,7 @@ public function testStringIntoFloat() : void */ public function testBooleanIntoFloat() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -464,6 +484,8 @@ public function testBooleanIntoFloat() : void $this->badValueWithMessage('Field "floatArgField" argument "floatArg" requires type Float, found true.', 4, 37), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } // Invalid Float values @@ -473,7 +495,7 @@ public function testBooleanIntoFloat() : void */ public function testUnquotedIntoFloat() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -486,6 +508,8 @@ public function testUnquotedIntoFloat() : void $this->badValueWithMessage('Field "floatArgField" argument "floatArg" requires type Float, found FOO.', 4, 37), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -493,7 +517,7 @@ public function testUnquotedIntoFloat() : void */ public function testIntIntoBoolean() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -506,6 +530,8 @@ public function testIntIntoBoolean() : void $this->badValueWithMessage('Field "booleanArgField" argument "booleanArg" requires type Boolean, found 2.', 4, 41), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -513,7 +539,7 @@ public function testIntIntoBoolean() : void */ public function testFloatIntoBoolean() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -526,6 +552,8 @@ public function testFloatIntoBoolean() : void $this->badValueWithMessage('Field "booleanArgField" argument "booleanArg" requires type Boolean, found 1.0.', 4, 41), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } // Invalid Boolean value @@ -535,7 +563,7 @@ public function testFloatIntoBoolean() : void */ public function testStringIntoBoolean() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -548,6 +576,8 @@ public function testStringIntoBoolean() : void $this->badValueWithMessage('Field "booleanArgField" argument "booleanArg" requires type Boolean, found "true".', 4, 41), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -555,7 +585,7 @@ public function testStringIntoBoolean() : void */ public function testUnquotedIntoBoolean() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -568,6 +598,8 @@ public function testUnquotedIntoBoolean() : void $this->badValueWithMessage('Field "booleanArgField" argument "booleanArg" requires type Boolean, found TRUE.', 4, 41), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -575,7 +607,7 @@ public function testUnquotedIntoBoolean() : void */ public function testFloatIntoID() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -588,6 +620,8 @@ public function testFloatIntoID() : void $this->badValueWithMessage('Field "idArgField" argument "idArg" requires type ID, found 1.0.', 4, 31), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -595,7 +629,7 @@ public function testFloatIntoID() : void */ public function testBooleanIntoID() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -608,6 +642,8 @@ public function testBooleanIntoID() : void $this->badValueWithMessage('Field "idArgField" argument "idArg" requires type ID, found true.', 4, 31), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } // Invalid ID value @@ -617,7 +653,7 @@ public function testBooleanIntoID() : void */ public function testUnquotedIntoID() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -630,6 +666,8 @@ public function testUnquotedIntoID() : void $this->badValueWithMessage('Field "idArgField" argument "idArg" requires type ID, found SOMETHING.', 4, 31), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -637,7 +675,7 @@ public function testUnquotedIntoID() : void */ public function testIntIntoEnum() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -650,6 +688,8 @@ public function testIntIntoEnum() : void $this->badValueWithMessage('Field "doesKnowCommand" argument "dogCommand" requires type DogCommand, found 2.', 4, 41), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -657,7 +697,7 @@ public function testIntIntoEnum() : void */ public function testFloatIntoEnum() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -670,6 +710,8 @@ public function testFloatIntoEnum() : void $this->badValueWithMessage('Field "doesKnowCommand" argument "dogCommand" requires type DogCommand, found 1.0.', 4, 41), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } // Invalid Enum value @@ -679,7 +721,7 @@ public function testFloatIntoEnum() : void */ public function testStringIntoEnum() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -692,6 +734,8 @@ public function testStringIntoEnum() : void $this->badValueWithMessage('Field "doesKnowCommand" argument "dogCommand" requires type DogCommand, found "SIT"; Did you mean the enum value SIT?', 4, 41), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -699,7 +743,7 @@ public function testStringIntoEnum() : void */ public function testBooleanIntoEnum() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -712,6 +756,8 @@ public function testBooleanIntoEnum() : void $this->badValueWithMessage('Field "doesKnowCommand" argument "dogCommand" requires type DogCommand, found true.', 4, 41), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -719,7 +765,7 @@ public function testBooleanIntoEnum() : void */ public function testUnknownEnumValueIntoEnum() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -732,6 +778,8 @@ public function testUnknownEnumValueIntoEnum() : void $this->badValueWithMessage('Field "doesKnowCommand" argument "dogCommand" requires type DogCommand, found JUGGLE.', 4, 41), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -739,7 +787,7 @@ public function testUnknownEnumValueIntoEnum() : void */ public function testDifferentCaseEnumValueIntoEnum() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -752,6 +800,8 @@ public function testDifferentCaseEnumValueIntoEnum() : void $this->badValueWithMessage('Field "doesKnowCommand" argument "dogCommand" requires type DogCommand, found sit; Did you mean the enum value SIT?', 4, 41), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -829,7 +879,7 @@ public function testSingleValueIntoList() : void */ public function testIncorrectItemtype() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -842,6 +892,8 @@ public function testIncorrectItemtype() : void $this->badValueWithMessage('Field "stringListArgField" argument "stringListArg" requires type String, found 2.', 4, 55), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -849,7 +901,7 @@ public function testIncorrectItemtype() : void */ public function testSingleValueOfIncorrectType() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -862,6 +914,8 @@ public function testSingleValueOfIncorrectType() : void $this->badValueWithMessage('Field "stringListArgField" argument "stringListArg" requires type [String], found 1.', 4, 47), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } // Invalid List value @@ -1043,7 +1097,7 @@ public function testAllReqsAndOptsOnMixedList() : void */ public function testIncorrectValueType() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -1057,14 +1111,17 @@ public function testIncorrectValueType() : void $this->badValueWithMessage('Field "multipleReqs" argument "req1" requires type Int!, found "one".', 4, 45), ] ); + + self::assertTrue($errors[0]->isClientSafe()); + self::assertTrue($errors[1]->isClientSafe()); } /** - * @see it('Incorrect value and missing argument (ProvidedNonNullArguments)') + * @see it('Incorrect value and missing argument (ProvidedRequiredArguments)') */ - public function testIncorrectValueAndMissingArgumentProvidedNonNullArguments() : void + public function testIncorrectValueAndMissingArgumentProvidedRequiredArguments() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -1077,6 +1134,8 @@ public function testIncorrectValueAndMissingArgumentProvidedNonNullArguments() : $this->badValueWithMessage('Field "multipleReqs" argument "req1" requires type Int!, found "one".', 4, 32), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } // Invalid non-nullable value @@ -1086,7 +1145,7 @@ public function testIncorrectValueAndMissingArgumentProvidedNonNullArguments() : */ public function testNullValue2() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -1099,6 +1158,8 @@ public function testNullValue2() : void $this->badValueWithMessage('Field "multipleReqs" argument "req1" requires type Int!, found null.', 4, 32), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -1222,7 +1283,7 @@ public function testFullObjectWithFieldsInDifferentOrder() : void */ public function testPartialObjectMissingRequired() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -1235,6 +1296,8 @@ public function testPartialObjectMissingRequired() : void $this->requiredField('ComplexInput', 'requiredField', 'Boolean!', 4, 41), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } private function requiredField($typeName, $fieldName, $fieldTypeName, $line, $column) @@ -1256,7 +1319,7 @@ private function requiredField($typeName, $fieldName, $fieldTypeName, $line, $co */ public function testPartialObjectInvalidFieldType() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -1272,6 +1335,31 @@ public function testPartialObjectInvalidFieldType() : void $this->badValueWithMessage('Field "complexArgField" argument "complexArg" requires type String, found 2.', 5, 40), ] ); + + self::assertTrue($errors[0]->isClientSafe()); + } + + /** + * @see it('Partial object, null to non-null field') + */ + public function testPartialObjectNullToNonNullField() + { + $errors = $this->expectFailsRule( + new ValuesOfCorrectType(), + ' + { + complicatedArgs { + complexArgField(complexArg: { + requiredField: true, + nonNullField: null, + }) + } + } + ', + [$this->badValueWithMessage('Field "complexArgField" argument "complexArg" requires type Boolean!, found null.', 6, 29)] + ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -1283,7 +1371,7 @@ public function testPartialObjectInvalidFieldType() : void */ public function testPartialObjectUnknownFieldArg() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -1300,11 +1388,12 @@ public function testPartialObjectUnknownFieldArg() : void 'ComplexInput', 'unknownField', 6, - 15, - 'Did you mean intField or booleanField?' + 15 ), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } private function unknownField($typeName, $fieldName, $line, $column, $message = null) @@ -1336,10 +1425,7 @@ public function testReportsOriginalErrorForCustomScalarWhichThrows() : void ] ); - self::assertEquals( - 'Field "invalidArg" argument "arg" requires type Invalid, found 123; Invalid scalar is always invalid: 123', - $errors[0]->getMessage() - ); + self::assertFalse($errors[0]->isClientSafe()); } /** @@ -1387,7 +1473,7 @@ public function testWithDirectivesOfValidTypes() : void */ public function testWithDirectiveWithIncorrectTypes() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' { @@ -1401,6 +1487,9 @@ public function testWithDirectiveWithIncorrectTypes() : void $this->badValueWithMessage('Field "name" argument "if" requires type Boolean!, found ENUM.', 4, 28), ] ); + + self::assertTrue($errors[0]->isClientSafe()); + self::assertTrue($errors[1]->isClientSafe()); } // DESCRIBE: Variable default values @@ -1417,6 +1506,7 @@ public function testVariablesWithValidDefaultValues() : void $a: Int = 1, $b: String = "ok", $c: ComplexInput = { requiredField: true, intField: 3 } + $d: Int! = 123 ) { dog { name } } @@ -1448,7 +1538,7 @@ public function testVariablesWithValidDefaultNullValues() : void */ public function testVariablesWithInvalidDefaultNullValues() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' query WithDefaultValues( @@ -1465,6 +1555,10 @@ public function testVariablesWithInvalidDefaultNullValues() : void $this->badValue('Boolean!', 'null', 5, 47), ] ); + + self::assertTrue($errors[0]->isClientSafe()); + self::assertTrue($errors[1]->isClientSafe()); + self::assertTrue($errors[2]->isClientSafe()); } /** @@ -1472,7 +1566,7 @@ public function testVariablesWithInvalidDefaultNullValues() : void */ public function testVariablesWithInvalidDefaultValues() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' query InvalidDefaultValues( @@ -1489,6 +1583,8 @@ public function testVariablesWithInvalidDefaultValues() : void $this->badValue('ComplexInput', '"notverycomplex"', 5, 30), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -1496,7 +1592,7 @@ public function testVariablesWithInvalidDefaultValues() : void */ public function testVariablesWithComplexInvalidDefaultValues() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' query WithDefaultValues( @@ -1510,6 +1606,8 @@ public function testVariablesWithComplexInvalidDefaultValues() : void $this->badValue('Int', '"abc"', 3, 62), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -1517,7 +1615,7 @@ public function testVariablesWithComplexInvalidDefaultValues() : void */ public function testComplexVariablesMissingRequiredField() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' query MissingRequiredField($a: ComplexInput = {intField: 3}) { @@ -1528,6 +1626,8 @@ public function testComplexVariablesMissingRequiredField() : void $this->requiredField('ComplexInput', 'requiredField', 'Boolean!', 2, 55), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } /** @@ -1535,7 +1635,7 @@ public function testComplexVariablesMissingRequiredField() : void */ public function testListVariablesWithInvalidItem() : void { - $this->expectFailsRule( + $errors = $this->expectFailsRule( new ValuesOfCorrectType(), ' query InvalidItem($a: [String] = ["one", 2]) { @@ -1546,5 +1646,7 @@ public function testListVariablesWithInvalidItem() : void $this->badValue('String', '2', 2, 50), ] ); + + self::assertTrue($errors[0]->isClientSafe()); } } diff --git a/tests/Validator/VariablesAreInputTypesTest.php b/tests/Validator/VariablesAreInputTypesTest.php index 52643140b..bd010e2bf 100644 --- a/tests/Validator/VariablesAreInputTypesTest.php +++ b/tests/Validator/VariablesAreInputTypesTest.php @@ -11,6 +11,7 @@ class VariablesAreInputTypesTest extends ValidatorTestCase { // Validate: Variables are input types + /** * @see it('input types are valid') */ diff --git a/tests/Validator/VariablesDefaultValueAllowedTest.php b/tests/Validator/VariablesDefaultValueAllowedTest.php deleted file mode 100644 index ddebae2b8..000000000 --- a/tests/Validator/VariablesDefaultValueAllowedTest.php +++ /dev/null @@ -1,132 +0,0 @@ -expectPassesRule( - new VariablesDefaultValueAllowed(), - ' - query NullableValues($a: Int, $b: String, $c: ComplexInput) { - dog { name } - } - ' - ); - } - - // DESCRIBE: Validate: Variable default value is allowed - - /** - * @see it('required variables without default values') - */ - public function testRequiredVariablesWithoutDefaultValues() : void - { - $this->expectPassesRule( - new VariablesDefaultValueAllowed(), - ' - query RequiredValues($a: Int!, $b: String!) { - dog { name } - } - ' - ); - } - - /** - * @see it('variables with valid default values') - */ - public function testVariablesWithValidDefaultValues() : void - { - $this->expectPassesRule( - new VariablesDefaultValueAllowed(), - ' - query WithDefaultValues( - $a: Int = 1, - $b: String = "ok", - $c: ComplexInput = { requiredField: true, intField: 3 } - ) { - dog { name } - } - ' - ); - } - - /** - * @see it('variables with valid default null values') - */ - public function testVariablesWithValidDefaultNullValues() : void - { - $this->expectPassesRule( - new VariablesDefaultValueAllowed(), - ' - query WithDefaultValues( - $a: Int = null, - $b: String = null, - $c: ComplexInput = { requiredField: true, intField: null } - ) { - dog { name } - } - ' - ); - } - - /** - * @see it('no required variables with default values') - */ - public function testNoRequiredVariablesWithDefaultValues() : void - { - $this->expectFailsRule( - new VariablesDefaultValueAllowed(), - ' - query UnreachableDefaultValues($a: Int! = 3, $b: String! = "default") { - dog { name } - } - ', - [ - $this->defaultForRequiredVar('a', 'Int!', 'Int', 2, 49), - $this->defaultForRequiredVar('b', 'String!', 'String', 2, 66), - ] - ); - } - - private function defaultForRequiredVar($varName, $typeName, $guessTypeName, $line, $column) - { - return FormattedError::create( - VariablesDefaultValueAllowed::defaultForRequiredVarMessage( - $varName, - $typeName, - $guessTypeName - ), - [new SourceLocation($line, $column)] - ); - } - - /** - * @see it('variables with invalid default null values') - */ - public function testNullIntoNullableType() : void - { - $this->expectFailsRule( - new VariablesDefaultValueAllowed(), - ' - query WithDefaultValues($a: Int! = null, $b: String! = null) { - dog { name } - } - ', - [ - $this->defaultForRequiredVar('a', 'Int!', 'Int', 2, 42), - $this->defaultForRequiredVar('b', 'String!', 'String', 2, 62), - ] - ); - } -} diff --git a/tests/Validator/VariablesInAllowedPositionTest.php b/tests/Validator/VariablesInAllowedPositionTest.php index b1a1edd5c..94a3cde11 100644 --- a/tests/Validator/VariablesInAllowedPositionTest.php +++ b/tests/Validator/VariablesInAllowedPositionTest.php @@ -11,6 +11,7 @@ class VariablesInAllowedPositionTest extends ValidatorTestCase { // Validate: Variables are in allowed positions + /** * @see it('Boolean => Boolean') */ @@ -109,25 +110,6 @@ public function testBooleanNonNullXBooleanWithinFragment() : void ); } - /** - * @see it('Int => Int! with default') - */ - public function testIntXIntNonNullWithDefault() : void - { - // Int => Int! with default - $this->expectPassesRule( - new VariablesInAllowedPosition(), - ' - query Query($intArg: Int = 1) - { - complicatedArgs { - nonNullIntArgField(nonNullIntArg: $intArg) - } - } - ' - ); - } - /** * @see it('[String] => [String]') */ @@ -252,22 +234,6 @@ public function testBooleanNonNullXBooleanNonNullInDirective() : void ); } - /** - * @see it('Boolean => Boolean! in directive with default') - */ - public function testBooleanXBooleanNonNullInDirectiveWithDefault() : void - { - $this->expectPassesRule( - new VariablesInAllowedPosition(), - ' - query Query($boolVar: Boolean = false) - { - dog @include(if: $boolVar) - } - ' - ); - } - /** * @see it('Int => Int!') */ @@ -463,4 +429,74 @@ public function testStringArrayXStringNonNullArray() : void ] ); } + + // Allows optional (nullable) variables with default values + + /** + * @see it('Int => Int! fails when variable provides null default value') + */ + public function testIntXIntNonNullFailsWhenVariableProvidesNullDefaultValue() + { + $this->expectFailsRule( + new VariablesInAllowedPosition(), + ' + query Query($intVar: Int = null) { + complicatedArgs { + nonNullIntArgField(nonNullIntArg: $intVar) + } + } + ', + [FormattedError::create( + VariablesInAllowedPosition::badVarPosMessage('intVar', 'Int', 'Int!'), + [new SourceLocation(2, 21), new SourceLocation(4, 47)] + ), + ] + ); + } + + /** + * @see it('Int => Int! when variable provides non-null default value') + */ + public function testIntXIntNonNullWhenVariableProvidesNonNullDefaultValue() + { + $this->expectPassesRule( + new VariablesInAllowedPosition(), + ' + query Query($intVar: Int = 1) { + complicatedArgs { + nonNullIntArgField(nonNullIntArg: $intVar) + } + }' + ); + } + + /** + * @see it('Int => Int! when optional argument provides default value') + */ + public function testIntXIntNonNullWhenOptionalArgumentProvidesDefaultValue() + { + $this->expectPassesRule( + new VariablesInAllowedPosition(), + ' + query Query($intVar: Int) { + complicatedArgs { + nonNullFieldWithDefault(nonNullIntArg: $intVar) + } + }' + ); + } + + /** + * @see it('Boolean => Boolean! in directive with default value with option') + */ + public function testBooleanXBooleanNonNullInDirectiveWithDefaultValueWithOption() + { + $this->expectPassesRule( + new VariablesInAllowedPosition(), + ' + query Query($boolVar: Boolean = false) { + dog @include(if: $boolVar) + }' + ); + } } diff --git a/tools/gendocs.php b/tools/gendocs.php index d28b0c93d..4acd4eb11 100644 --- a/tools/gendocs.php +++ b/tools/gendocs.php @@ -20,10 +20,10 @@ \GraphQL\Executor\ExecutionResult::class, \GraphQL\Executor\Promise\PromiseAdapter::class, \GraphQL\Validator\DocumentValidator::class, - \GraphQL\Error\Error::class => ['constants' => true, 'methods' => true, 'props' => true], - \GraphQL\Error\Warning::class => ['constants' => true, 'methods' => true], + \GraphQL\Error\Error::class => ['constants' => true, 'methods' => true, 'props' => true], + \GraphQL\Error\Warning::class => ['constants' => true, 'methods' => true], \GraphQL\Error\ClientAware::class, - \GraphQL\Error\Debug::class => ['constants' => true], + \GraphQL\Error\DebugFlag::class => ['constants' => true], \GraphQL\Error\FormattedError::class, \GraphQL\Server\StandardServer::class, \GraphQL\Server\ServerConfig::class, @@ -40,7 +40,9 @@ function renderClassMethod(ReflectionMethod $method) { $def = $type . '$' . $p->getName(); if ($p->isDefaultValueAvailable()) { - $val = $p->isDefaultValueConstant() ? $p->getDefaultValueConstantName() : $p->getDefaultValue(); + $val = $p->isDefaultValueConstant() + ? $p->getDefaultValueConstantName() + : $p->getDefaultValue(); $def .= " = " . Utils::printSafeJson($val); } @@ -50,8 +52,10 @@ function renderClassMethod(ReflectionMethod $method) { if (strlen($argsStr) >= 80) { $argsStr = "\n " . implode(",\n ", $args) . "\n"; } + $returnType = $method->getReturnType(); $def = "function {$method->getName()}($argsStr)"; $def = $method->isStatic() ? "static $def" : $def; + $def = $returnType ? "$def: $returnType" : $def; $docBlock = unpadDocblock($method->getDocComment()); return <<