Repository: digiaonline/graphql-php
Branch: main
Commit: 0397520e82e7
Files: 414
Total size: 1.6 MB
Directory structure:
gitextract_sov667i9/
├── .github/
│ ├── CONTRIBUTING.md
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ └── test.yml
├── .gitignore
├── .idea/
│ ├── codeStyles/
│ │ ├── Project.xml
│ │ └── codeStyleConfig.xml
│ ├── graphql-php.iml
│ ├── inspectionProfiles/
│ │ └── Project_Default.xml
│ ├── modules.xml
│ ├── php.xml
│ └── vcs.xml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── composer.json
├── phpunit.xml.dist
├── src/
│ ├── Error/
│ │ ├── AbstractException.php
│ │ ├── FileNotFoundException.php
│ │ ├── GraphQLException.php
│ │ ├── Handler/
│ │ │ ├── AbstractErrorMiddleware.php
│ │ │ ├── CallableMiddleware.php
│ │ │ ├── ErrorHandler.php
│ │ │ ├── ErrorHandlerInterface.php
│ │ │ └── ErrorMiddlewareInterface.php
│ │ ├── InvalidTypeException.php
│ │ ├── InvariantException.php
│ │ └── helpers.php
│ ├── Execution/
│ │ ├── CoercedValue.php
│ │ ├── Execution.php
│ │ ├── ExecutionContext.php
│ │ ├── ExecutionException.php
│ │ ├── ExecutionInterface.php
│ │ ├── ExecutionProvider.php
│ │ ├── ExecutionResult.php
│ │ ├── InvalidReturnTypeException.php
│ │ ├── Path.php
│ │ ├── ResolveInfo.php
│ │ ├── Strategy/
│ │ │ ├── AbstractExecutionStrategy.php
│ │ │ ├── ExecutionStrategyInterface.php
│ │ │ ├── FieldCollector.php
│ │ │ ├── ParallelExecutionStrategy.php
│ │ │ └── SerialExecutionStrategy.php
│ │ ├── UndefinedFieldException.php
│ │ └── ValuesResolver.php
│ ├── GraphQL.php
│ ├── Language/
│ │ ├── DirectiveLocationEnum.php
│ │ ├── FileSourceBuilder.php
│ │ ├── KeywordEnum.php
│ │ ├── LanguageException.php
│ │ ├── LanguageProvider.php
│ │ ├── Lexer.php
│ │ ├── LexerInterface.php
│ │ ├── Location.php
│ │ ├── MultiFileSourceBuilder.php
│ │ ├── Node/
│ │ │ ├── ASTNodeAwareInterface.php
│ │ │ ├── ASTNodeTrait.php
│ │ │ ├── AbstractNode.php
│ │ │ ├── AliasTrait.php
│ │ │ ├── ArgumentNode.php
│ │ │ ├── ArgumentsAwareInterface.php
│ │ │ ├── ArgumentsTrait.php
│ │ │ ├── BooleanValueNode.php
│ │ │ ├── DefaultValueTrait.php
│ │ │ ├── DefinitionNodeInterface.php
│ │ │ ├── DescriptionTrait.php
│ │ │ ├── DirectiveDefinitionNode.php
│ │ │ ├── DirectiveNode.php
│ │ │ ├── DirectivesAwareInterface.php
│ │ │ ├── DirectivesTrait.php
│ │ │ ├── DocumentNode.php
│ │ │ ├── EnumTypeDefinitionNode.php
│ │ │ ├── EnumTypeExtensionNode.php
│ │ │ ├── EnumValueDefinitionNode.php
│ │ │ ├── EnumValueNode.php
│ │ │ ├── EnumValuesTrait.php
│ │ │ ├── ExecutableDefinitionNodeInterface.php
│ │ │ ├── FieldDefinitionNode.php
│ │ │ ├── FieldNode.php
│ │ │ ├── FieldsTrait.php
│ │ │ ├── FloatValueNode.php
│ │ │ ├── FragmentDefinitionNode.php
│ │ │ ├── FragmentNodeInterface.php
│ │ │ ├── FragmentSpreadNode.php
│ │ │ ├── InlineFragmentNode.php
│ │ │ ├── InputArgumentsTrait.php
│ │ │ ├── InputFieldsTrait.php
│ │ │ ├── InputObjectTypeDefinitionNode.php
│ │ │ ├── InputObjectTypeExtensionNode.php
│ │ │ ├── InputValueDefinitionNode.php
│ │ │ ├── IntValueNode.php
│ │ │ ├── InterfaceTypeDefinitionNode.php
│ │ │ ├── InterfaceTypeExtensionNode.php
│ │ │ ├── InterfacesTrait.php
│ │ │ ├── ListTypeNode.php
│ │ │ ├── ListValueNode.php
│ │ │ ├── NameAwareInterface.php
│ │ │ ├── NameNode.php
│ │ │ ├── NameTrait.php
│ │ │ ├── NamedTypeNode.php
│ │ │ ├── NamedTypeNodeInterface.php
│ │ │ ├── NodeInterface.php
│ │ │ ├── NodeKindEnum.php
│ │ │ ├── NonNullTypeNode.php
│ │ │ ├── NullValueNode.php
│ │ │ ├── ObjectFieldNode.php
│ │ │ ├── ObjectTypeDefinitionNode.php
│ │ │ ├── ObjectTypeExtensionNode.php
│ │ │ ├── ObjectValueNode.php
│ │ │ ├── OperationDefinitionNode.php
│ │ │ ├── OperationTypeDefinitionNode.php
│ │ │ ├── ScalarTypeDefinitionNode.php
│ │ │ ├── ScalarTypeExtensionNode.php
│ │ │ ├── SchemaDefinitionNode.php
│ │ │ ├── SchemaExtensionNode.php
│ │ │ ├── SelectionNodeInterface.php
│ │ │ ├── SelectionSetAwareInterface.php
│ │ │ ├── SelectionSetNode.php
│ │ │ ├── SelectionSetTrait.php
│ │ │ ├── StringValueNode.php
│ │ │ ├── TypeConditionTrait.php
│ │ │ ├── TypeNodeInterface.php
│ │ │ ├── TypeSystemDefinitionNodeInterface.php
│ │ │ ├── TypeSystemExtensionNodeInterface.php
│ │ │ ├── TypeTrait.php
│ │ │ ├── TypesTrait.php
│ │ │ ├── UnionTypeDefinitionNode.php
│ │ │ ├── UnionTypeExtensionNode.php
│ │ │ ├── ValueAwareInterface.php
│ │ │ ├── ValueLiteralTrait.php
│ │ │ ├── ValueNodeInterface.php
│ │ │ ├── ValueTrait.php
│ │ │ ├── VariableDefinitionNode.php
│ │ │ ├── VariableDefinitionsAwareInterface.php
│ │ │ ├── VariableDefinitionsTrait.php
│ │ │ └── VariableNode.php
│ │ ├── NodeBuilder.php
│ │ ├── NodeBuilderInterface.php
│ │ ├── NodePrinter.php
│ │ ├── NodePrinterInterface.php
│ │ ├── Parser.php
│ │ ├── ParserInterface.php
│ │ ├── PrintException.php
│ │ ├── Source.php
│ │ ├── SourceBuilderInterface.php
│ │ ├── SourceLocation.php
│ │ ├── StringSourceBuilder.php
│ │ ├── SyntaxErrorException.php
│ │ ├── Token.php
│ │ ├── TokenKindEnum.php
│ │ ├── Visitor/
│ │ │ ├── ParallelVisitor.php
│ │ │ ├── SpecificKindVisitor.php
│ │ │ ├── TypeInfoVisitor.php
│ │ │ ├── Visitor.php
│ │ │ ├── VisitorBreak.php
│ │ │ ├── VisitorInfo.php
│ │ │ ├── VisitorInterface.php
│ │ │ └── VisitorResult.php
│ │ ├── blockStringValue.php
│ │ └── utils.php
│ ├── Schema/
│ │ ├── Building/
│ │ │ ├── BuildInfo.php
│ │ │ ├── BuildingContext.php
│ │ │ ├── BuildingContextInterface.php
│ │ │ ├── SchemaBuilder.php
│ │ │ ├── SchemaBuilderInterface.php
│ │ │ ├── SchemaBuildingException.php
│ │ │ └── SchemaBuildingProvider.php
│ │ ├── DefinitionBuilder.php
│ │ ├── DefinitionBuilderInterface.php
│ │ ├── DefinitionInterface.php
│ │ ├── DefinitionPrinter.php
│ │ ├── DefinitionPrinterInterface.php
│ │ ├── Extension/
│ │ │ ├── ExtendInfo.php
│ │ │ ├── ExtensionContext.php
│ │ │ ├── ExtensionContextInterface.php
│ │ │ ├── SchemaExtender.php
│ │ │ ├── SchemaExtenderInterface.php
│ │ │ ├── SchemaExtensionException.php
│ │ │ └── SchemaExtensionProvider.php
│ │ ├── Resolver/
│ │ │ ├── AbstractFieldResolver.php
│ │ │ ├── AbstractTypeResolver.php
│ │ │ ├── ResolverCollectionInterface.php
│ │ │ ├── ResolverInterface.php
│ │ │ ├── ResolverMap.php
│ │ │ ├── ResolverMiddlewareInterface.php
│ │ │ ├── ResolverRegistry.php
│ │ │ ├── ResolverRegistryInterface.php
│ │ │ └── ResolverTrait.php
│ │ ├── Schema.php
│ │ ├── Validation/
│ │ │ ├── Rule/
│ │ │ │ ├── AbstractRule.php
│ │ │ │ ├── DirectivesRule.php
│ │ │ │ ├── RootTypesRule.php
│ │ │ │ ├── RuleInterface.php
│ │ │ │ ├── SupportedRules.php
│ │ │ │ └── TypesRule.php
│ │ │ ├── SchemaValidationException.php
│ │ │ ├── SchemaValidationProvider.php
│ │ │ ├── SchemaValidator.php
│ │ │ ├── SchemaValidatorInterface.php
│ │ │ ├── ValidationContext.php
│ │ │ └── ValidationContextInterface.php
│ │ └── utils.php
│ ├── Type/
│ │ ├── Coercer/
│ │ │ ├── AbstractCoercer.php
│ │ │ ├── BooleanCoercer.php
│ │ │ ├── CoercerInterface.php
│ │ │ ├── CoercingException.php
│ │ │ ├── FloatCoercer.php
│ │ │ ├── IntCoercer.php
│ │ │ └── StringCoercer.php
│ │ ├── CoercerProvider.php
│ │ ├── Definition/
│ │ │ ├── AbstractTypeInterface.php
│ │ │ ├── Argument.php
│ │ │ ├── ArgumentsAwareInterface.php
│ │ │ ├── ArgumentsTrait.php
│ │ │ ├── CompositeTypeInterface.php
│ │ │ ├── DefaultValue.php
│ │ │ ├── DefaultValueTrait.php
│ │ │ ├── DeprecationAwareInterface.php
│ │ │ ├── DeprecationTrait.php
│ │ │ ├── DescriptionAwareInterface.php
│ │ │ ├── DescriptionTrait.php
│ │ │ ├── Directive.php
│ │ │ ├── EnumType.php
│ │ │ ├── EnumValue.php
│ │ │ ├── ExtensionASTNodesTrait.php
│ │ │ ├── Field.php
│ │ │ ├── FieldInterface.php
│ │ │ ├── FieldsAwareInterface.php
│ │ │ ├── FieldsTrait.php
│ │ │ ├── InputField.php
│ │ │ ├── InputObjectType.php
│ │ │ ├── InputTypeInterface.php
│ │ │ ├── InputValueInterface.php
│ │ │ ├── InterfaceType.php
│ │ │ ├── LeafTypeInterface.php
│ │ │ ├── ListType.php
│ │ │ ├── NameTrait.php
│ │ │ ├── NamedTypeInterface.php
│ │ │ ├── NonNullType.php
│ │ │ ├── ObjectType.php
│ │ │ ├── OfTypeTrait.php
│ │ │ ├── OutputTypeInterface.php
│ │ │ ├── ResolveTrait.php
│ │ │ ├── ResolveTypeTrait.php
│ │ │ ├── ScalarType.php
│ │ │ ├── SerializableTypeInterface.php
│ │ │ ├── SpecifiedDirectiveEnum.php
│ │ │ ├── TypeInterface.php
│ │ │ ├── TypeNameEnum.php
│ │ │ ├── TypeTrait.php
│ │ │ ├── UnionType.php
│ │ │ ├── ValueTrait.php
│ │ │ └── WrappingTypeInterface.php
│ │ ├── DirectivesProvider.php
│ │ ├── IntrospectionProvider.php
│ │ ├── ScalarTypesProvider.php
│ │ ├── TypeKindEnum.php
│ │ ├── definition.php
│ │ ├── directives.php
│ │ ├── introspection.php
│ │ └── scalars.php
│ ├── Util/
│ │ ├── AbstractEnum.php
│ │ ├── ArrayToJsonTrait.php
│ │ ├── ConversionException.php
│ │ ├── NameHelper.php
│ │ ├── NodeComparator.php
│ │ ├── SerializationInterface.php
│ │ ├── TypeASTConverter.php
│ │ ├── TypeHelper.php
│ │ ├── TypeInfo.php
│ │ ├── ValueASTConverter.php
│ │ ├── ValueConverter.php
│ │ ├── ValueHelper.php
│ │ └── utils.php
│ ├── Validation/
│ │ ├── Conflict/
│ │ │ ├── ComparisonContext.php
│ │ │ ├── Conflict.php
│ │ │ ├── ConflictFinder.php
│ │ │ ├── FieldContext.php
│ │ │ └── PairSet.php
│ │ ├── Rule/
│ │ │ ├── AbstractRule.php
│ │ │ ├── ExecutableDefinitionsRule.php
│ │ │ ├── FieldOnCorrectTypeRule.php
│ │ │ ├── FragmentsOnCompositeTypesRule.php
│ │ │ ├── KnownArgumentNamesRule.php
│ │ │ ├── KnownDirectivesRule.php
│ │ │ ├── KnownFragmentNamesRule.php
│ │ │ ├── KnownTypeNamesRule.php
│ │ │ ├── LoneAnonymousOperationRule.php
│ │ │ ├── NoFragmentCyclesRule.php
│ │ │ ├── NoUndefinedVariablesRule.php
│ │ │ ├── NoUnusedFragmentsRule.php
│ │ │ ├── NoUnusedVariablesRule.php
│ │ │ ├── OverlappingFieldsCanBeMergedRule.php
│ │ │ ├── PossibleFragmentSpreadsRule.php
│ │ │ ├── ProvidedRequiredArgumentsRule.php
│ │ │ ├── RuleInterface.php
│ │ │ ├── ScalarLeafsRule.php
│ │ │ ├── SingleFieldSubscriptionsRule.php
│ │ │ ├── SupportedRules.php
│ │ │ ├── UniqueArgumentNamesRule.php
│ │ │ ├── UniqueDirectivesPerLocationRule.php
│ │ │ ├── UniqueFragmentNamesRule.php
│ │ │ ├── UniqueInputFieldNamesRule.php
│ │ │ ├── UniqueOperationNamesRule.php
│ │ │ ├── UniqueVariableNamesRule.php
│ │ │ ├── ValuesOfCorrectTypeRule.php
│ │ │ ├── VariablesAreInputTypesRule.php
│ │ │ ├── VariablesDefaultValueAllowedRule.php
│ │ │ └── VariablesInAllowedPositionRule.php
│ │ ├── RulesProvider.php
│ │ ├── ValidationContext.php
│ │ ├── ValidationContextAwareTrait.php
│ │ ├── ValidationContextInterface.php
│ │ ├── ValidationException.php
│ │ ├── ValidationExceptionInterface.php
│ │ ├── ValidationProvider.php
│ │ ├── Validator.php
│ │ ├── ValidatorInterface.php
│ │ └── messages.php
│ └── api.php
└── tests/
├── Functional/
│ ├── Execution/
│ │ ├── AbstractPromiseTest.php
│ │ ├── AbstractTest.php
│ │ ├── DeferredResolverTest.php
│ │ ├── DirectivesTest.php
│ │ ├── ExecutionTest.php
│ │ ├── ListTest.php
│ │ ├── MutationTest.php
│ │ ├── NonNullTest.php
│ │ ├── ResolveTest.php
│ │ ├── SchemaTest.php
│ │ ├── UnionInterfaceTest.php
│ │ ├── ValuesResolverTest.php
│ │ ├── VariablesTest.php
│ │ └── testClasses.php
│ ├── IntrospectionTest.php
│ ├── Language/
│ │ ├── FileSourceBuilderTest.php
│ │ ├── MultiFileSourceBuilderTest.php
│ │ ├── ParserTest.php
│ │ ├── SchemaParserTest.php
│ │ ├── SchemaPrinterTest.php
│ │ ├── StringSourceBuilderTest.php
│ │ ├── VisitorTest.php
│ │ ├── kitchen-sink.graphql
│ │ └── schema-kitchen-sink.graphqls
│ ├── QueryTest.php
│ ├── Schema/
│ │ ├── BuildingTest.php
│ │ ├── DefinitionPrinterTest.php
│ │ ├── ExtensionTest.php
│ │ ├── SchemaBuilderTest.php
│ │ ├── SchemaTest.php
│ │ └── ValidationTest.php
│ ├── Type/
│ │ ├── DefinitionTest.php
│ │ ├── EnumTypeTest.php
│ │ ├── IntrospectionTest.php
│ │ ├── PredicateTest.php
│ │ ├── SerializationTest.php
│ │ └── introspection.graphql
│ ├── Validation/
│ │ ├── MessagesTest.php
│ │ ├── Rule/
│ │ │ ├── ExecutableDefinitionsRuleTest.php
│ │ │ ├── FieldOnCorrectTypeRuleTest.php
│ │ │ ├── FragmentsOnCompositeTypesRuleTest.php
│ │ │ ├── KnownArgumentNamesRuleTest.php
│ │ │ ├── KnownDirectivesRuleTest.php
│ │ │ ├── KnownFragmentNamesRuleTest.php
│ │ │ ├── KnownTypeNamesRuleTest.php
│ │ │ ├── LoneAnonymousOperationRuleTest.php
│ │ │ ├── NoFragmentCyclesRuleTest.php
│ │ │ ├── NoUndefinedVariablesRuleTest.php
│ │ │ ├── NoUnusedFragmentsRuleTest.php
│ │ │ ├── NoUnusedVariablesRuleTest.php
│ │ │ ├── OverlappingFieldsCanBeMergedRuleTest.php
│ │ │ ├── PossibleFragmentSpreadsRuleTest.php
│ │ │ ├── ProvidedRequiredArgumentsRuleTest.php
│ │ │ ├── RuleTestCase.php
│ │ │ ├── ScalarLeafsRuleTest.php
│ │ │ ├── SingleFieldSubscriptionsRuleTest.php
│ │ │ ├── UniqueArgumentNamesRuleTest.php
│ │ │ ├── UniqueDirectivesPerLocationRuleTest.php
│ │ │ ├── UniqueFragmentNamesRuleTest.php
│ │ │ ├── UniqueInputFieldNamesRuleTest.php
│ │ │ ├── UniqueOperationNamesRuleTest.php
│ │ │ ├── UniqueVariableNamesRuleTest.php
│ │ │ ├── ValuesOfCorrectTypeRuleTest.php
│ │ │ ├── VariablesAreInputTypesRuleTest.php
│ │ │ ├── VariablesDefaultValueAllowedRuleTest.php
│ │ │ └── VariablesInAllowedPositionRuleTest.php
│ │ ├── ValidationTest.php
│ │ ├── errors.php
│ │ └── harness.php
│ ├── ValidationTest.php
│ ├── starWars.graphqls
│ ├── starWarsData.php
│ └── starWarsSchema.php
├── TestCase.php
├── Unit/
│ ├── Error/
│ │ ├── GraphQLExceptionTest.php
│ │ └── Handler/
│ │ └── ErrorHandlerTest.php
│ ├── Execution/
│ │ └── ExecutionResultTest.php
│ ├── Language/
│ │ ├── BlockStringValueTest.php
│ │ ├── LexerTest.php
│ │ └── NodeBuilderTest.php
│ ├── Schema/
│ │ └── Resolver/
│ │ └── ResolverRegistryTest.php
│ ├── Type/
│ │ └── Coercer/
│ │ ├── CoercerInterfaceTest.php
│ │ ├── FloatCoercerTest.php
│ │ ├── IntCoercerTest.php
│ │ └── StringCoercerTest.php
│ └── Util/
│ ├── AbstractEnumTest.php
│ ├── NameHelperTest.php
│ ├── NodeComparatorTest.php
│ ├── ValueASTConverterTest.php
│ ├── ValueConverterTest.php
│ └── ValueHelperTest.php
└── utils.php
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/CONTRIBUTING.md
================================================
# Contributing
Please note the following guidelines before submitting pull requests:
- All new features must be covered by unit tests
- Always create pull requests to the *main* branch
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
---
### What did you do?
### What did you expect to happen?
### What actually happened?
### What version of this project are you using?
Please include code that reproduces the issue. The best reproductions are self-contained scripts with minimal dependencies.
```php
code goes here
```
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context about the feature request here.
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
(Please read the [guidelines](.github/CONTRIBUTING.md) before creating PRs.)
Fixes #.
Changes proposed in this pull request:
*
*
*
================================================
FILE: .github/workflows/test.yml
================================================
name: Test
on: [push, pull_request, workflow_dispatch]
env:
FORCE_COLOR: 1
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
php-version: ["7.1", "7.2", "7.3", "7.4"]
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v3
- name: Set up PHP ${{ matrix.php-version }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
coverage: xdebug
- name: Install dependencies
run: |
composer self-update
composer install
- name: Tests
run: |
composer ci
- name: Upload coverage results to Coveralls
env:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
wget https://github.com/php-coveralls/php-coveralls/releases/download/v1.0.1/coveralls.phar -O coveralls.phar
php coveralls.phar -v
================================================
FILE: .gitignore
================================================
/.idea/dictionaries
/.idea/deployment.xml
/.idea/webServers.xml
/.idea/workspace.xml
/.idea/php-test-framework.xml
/coverage/
/vendor/
clover.xml
composer.lock
cachegrind.out.*
phpunit.xml
# TODO: Remove this once the printer has been implemented.
src/Language/Printer*
================================================
FILE: .idea/codeStyles/Project.xml
================================================
================================================
FILE: .idea/codeStyles/codeStyleConfig.xml
================================================
================================================
FILE: .idea/graphql-php.iml
================================================
================================================
FILE: .idea/inspectionProfiles/Project_Default.xml
================================================
================================================
FILE: .idea/modules.xml
================================================
================================================
FILE: .idea/php.xml
================================================
================================================
FILE: .idea/vcs.xml
================================================
================================================
FILE: CHANGELOG.md
================================================
# Change log
## 1.1.0
* Add initial support for execution strategies
* Add support for error handling middleware
* Fix some bugs in the error handling
* Fix some type-hint issues
* Run Travis CI tests on PHP 7.3 too, improve build times by caching
## 1.0.3
* Drastically reduce the number of container `make()` ([#332](https://github.com/digiaonline/graphql-php/pull/332))
## 1.0.2
* Expanded the README table of contents somewhat ([#329](https://github.com/digiaonline/graphql-php/pull/329))
* Don't validate the schema again while validating the query against it ([#328](https://github.com/digiaonline/graphql-php/pull/328))
* Fix some incorrect type-hints in `ExecutionResult` ([#327](https://github.com/digiaonline/graphql-php/pull/327))
* Fix resolver example in the README ([#313](https://github.com/digiaonline/graphql-php/pull/313))
## 1.0.1
* Fix a bug where you could not use `false` as value for `Boolean!` input types ([#311](https://github.com/digiaonline/graphql-php/pull/311))
* Add a code of conduct ([#312](https://github.com/digiaonline/graphql-php/pull/312))
* Fix resolver middleware example ([#309](https://github.com/digiaonline/graphql-php/pull/309))
* Introduce `VisitorInfo` concept ([#308](https://github.com/digiaonline/graphql-php/pull/308))
## 1.0.0
* Initial release
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at christofferniska@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Digia
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# GraphQL
[](https://github.com/digiaonline/graphql-php/actions)
[](https://coveralls.io/github/digiaonline/graphql-php?branch=main)
[](https://scrutinizer-ci.com/g/digiaonline/graphql-php/?branch=main)
[](https://raw.githubusercontent.com/digiaonline/graphql-php/main/LICENSE)
[](#backers)
[](#sponsors)
This is a PHP implementation of the [GraphQL specification](https://facebook.github.io/graphql/) based on the
JavaScript [reference implementation](https://github.com/graphql/graphql-js).
## Related projects
- [DateTime scalar](https://github.com/digiaonline/graphql-datetime-scalar-php)
- [Relay support](https://github.com/digiaonline/graphql-relay-php)
## Requirements
- PHP version >= 7.1
- ext-mbstring
## Table of contents
- [Installation](#installation)
- [Example](#example)
- [Creating a schema](#creating-a-schema)
- [Resolver registry](#resolver-registry)
- [Resolver middleware](#resolver-middleware)
- [Execution](#execution)
- [Queries](#queries)
- [Resolvers](#resolvers)
- [The N+1 problem](#the-n1-problem)
- [Variables](#variables)
- [Context](#context)
- [Scalars](#scalars)
- [Custom scalars](#custom-scalars)
- [Advanced usage](#advanced-usage)
- [Integration](#integration)
- [Laravel](#laravel)
## Installation
Run the following command to install the package through Composer:
```sh
composer require digiaonline/graphql
```
## Example
Here is a simple example that demonstrates how to build an executable schema from a GraphQL schema file that contains
the Schema Definition Language (SDL) for a Star Wars-themed schema (for the schema definition itself, see below). In
this example we use that SDL to build an executable schema and use it to query for the name of the hero. The result
of that query is an associative array with a structure that resembles the query we ran.
```php
use Digia\GraphQL\Language\FileSourceBuilder;
use function Digia\GraphQL\buildSchema;
use function Digia\GraphQL\graphql;
$sourceBuilder = new FileSourceBuilder(__DIR__ . '/star-wars.graphqls');
$schema = buildSchema($sourceBuilder->build(), [
'Query' => [
'hero' => function ($rootValue, $arguments) {
return getHero($arguments['episode'] ?? null);
},
],
]);
$result = graphql($schema, '
query HeroNameQuery {
hero {
name
}
}');
\print_r($result);
```
The script above produces the following output:
```php
Array
(
[data] => Array
(
[hero] => Array
(
[name] => "R2-D2"
)
)
)
```
The GraphQL schema file used in this example contains the following:
```graphql schema
schema {
query: Query
}
type Query {
hero(episode: Episode): Character
human(id: String!): Human
droid(id: String!): Droid
}
interface Character {
id: String!
name: String
friends: [Character]
appearsIn: [Episode]
}
type Human implements Character {
id: String!
name: String
friends: [Character]
appearsIn: [Episode]
homePlanet: String
}
type Droid implements Character {
id: String!
name: String
friends: [Character]
appearsIn: [Episode]
primaryFunction: String
}
enum Episode { NEWHOPE, EMPIRE, JEDI }
```
## Creating a schema
In order to execute queries against your GraphQL API, you first need to define the structure of your API. This is done
by creating a schema. There are two ways to do this, you can either do it using SDL or you can do it programmatically.
However, we strongly encourage you to use SDL, because it is easier to work with. To make an executable schema from
SDL you need to call the `buildSchema` function.
The `buildSchema` function takes three arguments:
- `$source` The schema definition (SDL) as a `Source` instance
- `$resolverRegistry` An associative array or a `ResolverRegistry` instance that contains all resolvers
- `$options` The options for building the schema, which also includes custom types and directives
To create the `Source` instance you can use the provided `FileSourceBuilder` or `MultiFileSourceBuilder` classes.
### Resolver registry
The resolver registry is essentially a flat map with the type names as its keys and their corresponding resolver
instances as its values. For smaller projects you can use an associative array and lambda functions to define your
resolver registry. However, in larger projects we suggest that you implement your own resolvers instead. You can read
more about resolvers under the [Resolvers](#resolvers) section.
Associative array example:
```php
$schema = buildSchema($source, [
'Query' => [
'hero' => function ($rootValue, $arguments) {
return getHero($arguments['episode'] ?? null);
},
],
]);
```
Resolver class example:
```php
$schema = buildSchema($source, [
'Query' => [
'hero' => new HeroResolver(),
],
]);
```
### Resolver middleware
If you find yourself writing the same logic in multiple resolvers you should consider using middleware. Resolver
middleware allow you to efficiently manage functionality across multiple resolvers.
Before middleware example:
```php
$resolverRegistry = new ResolverRegristry([
'Query' => [
'hero' => function ($rootValue, $arguments) {
return getHero($arguments['episode'] ?? null);
},
],
], [
'middleware' => [new BeforeMiddleware()],
]);
$schema = buildSchema($source, $resolverRegistry);
```
```php
class BeforeMiddleware implements ResolverMiddlewareInterface
{
public function resolve(callable $resolveCallback, $rootValue, array $arguments, $context, ResolveInfo $info) {
$newRootValue = $this->doSomethingBefore();
return $resolveCallback($newRootValue, $arguments, $context, $info);
}
}
```
After middleware example:
```php
$resolverRegistry = new ResolverRegristry([
'Query' => [
'hero' => function ($rootValue, $arguments) {
return getHero($arguments['episode'] ?? null);
},
],
], [
'middleware' => [new AfterMiddleware()],
]);
$schema = buildSchema($source, $resolverRegistry);
```
```php
class AfterMiddleware implements ResolverMiddlewareInterface
{
public function resolve(callable $resolveCallback, $rootValue, array $arguments, $context, ResolveInfo $info) {
$result = $resolveCallback($rootValue, $arguments, $context, $info);
$this->doSomethingAfter();
return $result;
}
}
```
Resolver middleware can be useful for a number of things; such as logging, input sanitization, performance
measurement, authorization and caching.
If you want to learn more about schemas you can refer to the [specification](https://graphql.org/learn/schema/).
## Execution
### Queries
To execute a query against your schema you need to call the `graphql` function and pass it your schema and the query
you wish to execute. You can also run _mutations_ and _subscriptions_ by changing your query.
```php
$query = '
query HeroNameQuery {
hero {
name
}
}';
$result = graphql($schema, $query);
```
If you want to learn more about queries you can refer to the [specification](https://graphql.org/learn/queries/).
### Resolvers
Each type in a schema has a resolver associated with it that allows for resolving the actual value. However, most
types do not need a custom resolver, because they can be resolved using the default resolver. Usually these resolvers
are lambda functions, but you can also define your own resolvers by extending `AbstractTypeResolver` or `AbstractFieldResolver`. Alternatively you can also implement the `ResolverInterface` directly.
A resolver function receives four arguments:
- `$rootValue` The parent object, which can also be `null` in some cases
- `$arguments` The arguments provided to the field in the query
- `$context` A value that is passed to every resolver that can hold important contextual information
- `$info` A value which holds field-specific information relevant to the current query
Lambda function example:
```php
function ($rootValue, array $arguments, $context, ResolveInfo $info): string {
return [
'type' => 'Human',
'id' => '1000',
'name' => 'Luke Skywalker',
'friends' => ['1002', '1003', '2000', '2001'],
'appearsIn' => ['NEWHOPE', 'EMPIRE', 'JEDI'],
'homePlanet' => 'Tatooine',
];
}
```
Type resolver example:
```php
class HumanResolver extends AbstractTypeResolver
{
public function resolveName($rootValue, array $arguments, $context, ResolveInfo $info): string
{
return $rootValue['name'];
}
}
```
Field resolver example:
```php
class NameResolver extends AbstractFieldResolver
{
public function resolve($rootValue, array $arguments, $context, ResolveInfo $info): string
{
return $rootValue['name'];
}
}
```
#### The N+1 problem
The resolver function can return a value, a [promise](https://github.com/reactphp/promise) or an array of promises.
This resolver function below illustrates how to use promise to solve the N+1 problem, the full example can be found in
this [test case](/tests/Functional/Execution/DeferredResolverTest.php).
```php
$movieType = newObjectType([
'fields' => [
'title' => ['type' => stringType()],
'director' => [
'type' => $directorType,
'resolve' => function ($movie, $args) {
DirectorBuffer::add($movie['directorId']);
return new Promise(function (callable $resolve, callable $reject) use ($movie) {
DirectorBuffer::loadBuffered();
$resolve(DirectorBuffer::get($movie['directorId']));
});
}
]
]
]);
```
### Variables
You can pass in variables when executing a query by passing them to the `graphql` function.
```php
$query = '
query HeroNameQuery($id: ID!) {
hero(id: $id) {
name
}
}';
$variables = ['id' => '1000'];
$result = graphql($schema, $query, null, null, $variables);
```
### Context
In case you need to pass in some important contextual information to your queries you can use the `$contextValues`
argument on `graphql` to do so. This data will be passed to all of your resolvers as the `$context` argument.
```php
$contextValues = [
'currentlyLoggedInUser' => $currentlyLoggedInUser,
];
$result = graphql($schema, $query, null, $contextValues, $variables);
```
## Scalars
The leaf nodes in a schema are called scalars and each scalar resolves to some concrete data. The built-in, or
specified scalars in GraphQL are the following:
- Boolean
- Float
- Int
- ID
- String
### Custom scalars
In addition to the specified scalars you can also define your own custom scalars and let your schema know about
them by passing them to the `buildSchema` function as part of its `$options` argument.
Custom Date scalar type example:
```php
$dateType = newScalarType([
'name' => 'Date',
'serialize' => function ($value) {
if ($value instanceof DateTime) {
return $value->format('Y-m-d');
}
return null;
},
'parseValue' => function ($value) {
if (\is_string($value)){
return new DateTime($value);
}
return null;
},
'parseLiteral' => function ($node) {
if ($node instanceof StringValueNode) {
return new DateTime($node->getValue());
}
return null;
},
]);
$schema = buildSchema($source, [
'Query' => QueryResolver::class,
[
'types' => [$dateType],
],
]);
```
Every scalar has to be coerced, which is done by three different functions. The `serialize` function converts a
PHP value into the corresponding output value. The`parseValue` function converts a variable input value into the
corresponding PHP value and the `parseLiteral` function converts an AST literal into the corresponding PHP value.
## Advanced usage
If you are looking for something that isn't yet covered by this documentation your best bet is to take a look at the
[tests](./tests) in this project. You'll be surprised how many examples you'll find there.
## Integration
### Laravel
Here is an example that demonstrates how you can use this library in your Laravel project. You need an application
service to expose this library to your application, a service provider to register that service, a controller and a
route for handling the GraphQL POST requests.
**app/GraphQL/GraphQLService.php**
```php
class GraphQLService
{
private $schema;
public function __construct(Schema $schema)
{
$this->schema = $schema;
}
public function executeQuery(string $query, array $variables, ?string $operationName): array
{
return graphql($this->schema, $query, null, null, $variables, $operationName);
}
}
```
**app/GraphQL/GraphQLServiceProvider.php**
```php
class GraphQLServiceProvider
{
public function register()
{
$this->app->singleton(GraphQLService::class, function () {
$schemaDef = \file_get_contents(__DIR__ . '/schema.graphqls');
$executableSchema = buildSchema($schemaDef, [
'Query' => QueryResolver::class,
]);
return new GraphQLService($executableSchema);
});
}
}
```
**app/GraphQL/GraphQLController.php**
```php
class GraphQLController extends Controller
{
private $graphqlService;
public function __construct(GraphQLService $graphqlService)
{
$this->graphqlService = $graphqlService;
}
public function handle(Request $request): JsonResponse
{
$query = $request->get('query');
$variables = $request->get('variables') ?? [];
$operationName = $request->get('operationName');
$result = $this->graphqlService->executeQuery($query, $variables, $operationName);
return response()->json($result);
}
}
```
**routes/api.php**
```php
Route::post('/graphql', 'app\GraphQL\GraphQLController@handle');
```
## Contributors
This project exists thanks to all the people who contribute. [Contribute](.github/CONTRIBUTING.md).
## Backers
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/graphql-php#backer)
## Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](https://opencollective.com/graphql-php#sponsor)
## License
See [LICENCE](LICENSE).
================================================
FILE: composer.json
================================================
{
"name": "digiaonline/graphql",
"description": "A PHP7 implementation of the GraphQL specifications.",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Christoffer Niska",
"email": "christofferniska@gmail.com"
},
{
"name": "Hung Nguyen",
"email": "hungneox@gmail.com"
},
{
"name": "Sam Stenvall",
"email": "sam.stenvall@digia.com"
}
],
"require": {
"php": ">=7.1",
"ext-mbstring": "*",
"league/container": "^3.2",
"react/promise": "^2.5"
},
"require-dev": {
"phpunit/phpunit": "^7.0",
"phpstan/phpstan": "^0.9.2"
},
"autoload": {
"files": [
"./src/Error/helpers.php",
"./src/Language/blockStringValue.php",
"./src/Language/utils.php",
"./src/Schema/utils.php",
"./src/Type/definition.php",
"./src/Type/directives.php",
"./src/Type/introspection.php",
"./src/Type/scalars.php",
"./src/Util/utils.php",
"./src/Validation/messages.php",
"./src/api.php"
],
"psr-4": {
"Digia\\GraphQL\\": "./src"
}
},
"scripts": {
"test": [
"phpunit",
"phpstan analyse -l 4 src/"
],
"ci": [
"phpunit --coverage-clover build/logs/clover.xml",
"phpstan analyse -l 4 src/"
]
},
"autoload-dev": {
"files": [
"./tests/Functional/Execution/testClasses.php",
"./tests/Functional/Validation/errors.php",
"./tests/Functional/Validation/harness.php",
"./tests/Functional/starWarsData.php",
"./tests/Functional/starWarsSchema.php",
"./tests/utils.php"
],
"psr-4": {
"Digia\\GraphQL\\Test\\": "./tests"
}
}
}
================================================
FILE: phpunit.xml.dist
================================================
tests
src
================================================
FILE: src/Error/AbstractException.php
================================================
resolveNodes($nodes);
$this->resolveSource($source);
$this->resolvePositions($positions);
$this->resolveLocations($positions, $source);
$this->path = $path;
$this->extensions = $extensions;
$this->originalException = $originalException;
}
/**
* @return NodeInterface[]
*/
public function getNodes(): ?array
{
return $this->nodes;
}
/**
* @return bool
*/
public function hasSource(): bool
{
return null !== $this->source;
}
/**
* @return Source|null
*/
public function getSource(): ?Source
{
return $this->source;
}
/**
* @return int[]|null
*/
public function getPositions(): ?array
{
return $this->positions;
}
/**
* @return bool
*/
public function hasLocations(): bool
{
return !empty($this->locations);
}
/**
* @return array|null
*/
public function getLocations(): ?array
{
return $this->locations;
}
/**
* @return array|null
*/
public function getLocationsAsArray(): ?array
{
return !empty($this->locations) ? \array_map(function (SourceLocation $location) {
return $location->toArray();
}, $this->locations) : null;
}
/**
* @return array|null
*/
public function getPath(): ?array
{
return $this->path;
}
/**
* @return array|null
*/
public function getExtensions(): ?array
{
return $this->extensions;
}
/**
* @param array|null $extensions
* @return self
*/
public function setExtensions(?array $extensions): self
{
$this->extensions = $extensions;
return $this;
}
/**
* @return \Throwable|null
*/
public function getOriginalException(): ?\Throwable
{
return $this->originalException;
}
/**
* @return null|string
*/
public function getOriginalErrorMessage(): ?string
{
return null !== $this->originalException ? $this->originalException->getMessage() : null;
}
/**
* @inheritdoc
*/
public function toArray(): array
{
$result = [
'message' => $this->message,
// TODO: Do not include `locations` if `null` (similar to `path` and `extensions`).
'locations' => $this->getLocationsAsArray(),
];
if (null !== $this->path) {
$result['path'] = $this->path;
}
if (null !== $this->extensions) {
$result['extensions'] = $this->extensions;
}
return $result;
}
/**
* @inheritdoc
*/
public function __toString(): string
{
return printError($this);
}
/**
* @param array|null $nodes
* @return $this
*/
protected function resolveNodes(?array $nodes): self
{
if (\is_array($nodes)) {
$nodes = !empty($nodes) ? $nodes : [];
} else {
$nodes = [$nodes];
}
$this->nodes = \array_filter($nodes, function ($node) {
return null !== $node;
});
return $this;
}
/**
* @param Source|null $source
* @return $this
*/
protected function resolveSource(?Source $source): self
{
if (null === $source && !empty($this->nodes)) {
$firstNode = $this->nodes[0] ?? null;
$location = null !== $firstNode ? $firstNode->getLocation() : null;
$source = null !== $location ? $location->getSource() : null;
}
$this->source = $source;
return $this;
}
/**
* @param array|null $positions
* @return $this
*/
protected function resolvePositions(?array $positions): self
{
if (null === $positions && !empty($this->nodes)) {
$positions = \array_reduce($this->nodes, function (array $list, ?NodeInterface $node) {
if (null !== $node) {
$location = $node->getLocation();
if (null !== $location) {
$list[] = $location->getStart();
}
}
return $list;
}, []);
}
if (null !== $positions && empty($positions)) {
$positions = null;
}
$this->positions = $positions;
return $this;
}
/**
* @param array|null $positions
* @param Source|null $source
* @return $this
*/
protected function resolveLocations(?array $positions, ?Source $source): self
{
$locations = null;
if (null !== $positions && null !== $source) {
$locations = \array_map(function ($position) use ($source) {
return SourceLocation::fromSource($source, $position);
}, $positions);
} elseif (!empty($this->nodes)) {
$locations = \array_reduce($this->nodes, function (array $list, NodeInterface $node) {
$location = $node->getLocation();
if (null !== $location) {
$list[] = SourceLocation::fromSource($location->getSource(), $location->getStart());
}
return $list;
}, []);
}
if ($locations !== null) {
$this->locations = $locations;
}
return $this;
}
}
================================================
FILE: src/Error/Handler/AbstractErrorMiddleware.php
================================================
handleCallback = $handleCallback;
}
/**
* @inheritdoc
*/
public function handleExecutionError(ExecutionException $exception, ExecutionContext $context, callable $next)
{
\call_user_func($this->handleCallback, $exception, $context, $next);
}
}
================================================
FILE: src/Error/Handler/ErrorHandler.php
================================================
addMiddleware($mw);
}
}
/**
* @param \Throwable $exception
*/
public function handleError(\Throwable $exception): void
{
$next = function () {
// NO-OP
};
foreach ($this->middleware as $middleware) {
$next = function (\Throwable $exception) use ($middleware, $next) {
return $middleware->handleError($exception, $next);
};
}
$next($exception);
}
/**
* @inheritdoc
*/
public function handleExecutionError(ExecutionException $exception, ExecutionContext $context): void
{
$next = function () {
// NO-OP
};
foreach ($this->middleware as $middleware) {
$next = function (ExecutionException $exception, ExecutionContext $context) use ($middleware, $next) {
return $middleware->handleExecutionError($exception, $context, $next);
};
}
$next($exception, $context);
}
/**
* @param ErrorMiddlewareInterface $middleware
*/
protected function addMiddleware(ErrorMiddlewareInterface $middleware): void
{
\array_unshift($this->middleware, $middleware);
}
}
================================================
FILE: src/Error/Handler/ErrorHandlerInterface.php
================================================
$error->getMessage(),
'locations' => $error->getLocationsAsArray(),
'path' => $error->getPath(),
];
}
// Print error
/**
* @param GraphQLException $error
* @return string
*/
function printError(GraphQLException $error): string
{
$printedLocations = [];
$nodes = $error->getNodes();
if (!empty($nodes)) {
foreach ($nodes as $node) {
$location = $node->getLocation();
if (null !== $location) {
$printedLocations[] = highlightSourceAtLocation(
$location->getSource(),
SourceLocation::fromSource($location->getSource(), $location->getStart())
);
}
}
} elseif ($error->hasSource() && $error->hasLocations()) {
foreach ($error->getLocations() as $location) {
$printedLocations[] = highlightSourceAtLocation($error->getSource(), $location);
}
}
return empty($printedLocations)
? $error->getMessage()
: \implode("\n\n", \array_merge([$error->getMessage()], $printedLocations)) . "\n";
}
/**
* @param Source $source
* @param SourceLocation $location
* @return string
*/
function highlightSourceAtLocation(Source $source, SourceLocation $location): string
{
$line = $location->getLine();
$locationOffset = $source->getLocationOffset();
$lineOffset = $locationOffset->getLine() - 1;
$columnOffset = getColumnOffset($source, $location);
$contextLine = $line + $lineOffset;
$contextColumn = $location->getColumn() + $columnOffset;
$prevLineNum = (string)($contextLine - 1);
$lineNum = (string)$contextLine;
$nextLineNum = (string)($contextLine + 1);
$padLen = \mb_strlen($nextLineNum);
$lines = \preg_split("/\r\n|[\n\r]/", $source->getBody());
$lines = false === $lines ? [] : $lines;
$lines[0] = whitespace($locationOffset->getColumn() - 1) . $lines[0];
$outputLines = [
\sprintf('%s (%s:%s)', $source->getName(), $contextLine, $contextColumn),
$line >= 2 ? leftPad($padLen, $prevLineNum) . ': ' . $lines[$line - 2] : null,
leftPad($padLen, $lineNum) . ': ' . $lines[$line - 1],
whitespace(2 + $padLen + $contextColumn - 1) . '^',
$line < \count($lines) ? leftPad($padLen, $nextLineNum) . ': ' . $lines[$line] : null,
];
return \implode("\n", \array_filter($outputLines, function ($line) {
return null !== $line;
}));
}
/**
* @param Source $source
* @param SourceLocation $location
* @return int
*/
function getColumnOffset(Source $source, SourceLocation $location): int
{
return $location->getLine() === 1 ? $source->getLocationOffset()->getColumn() - 1 : 0;
}
/**
* @param int $length
* @return string
*/
function whitespace(int $length): string
{
return \str_repeat(' ', $length);
}
/**
* @param int $length
* @param string $str
* @return string
*/
function leftPad(int $length, string $str): string
{
return whitespace($length - \mb_strlen($str)) . $str;
}
================================================
FILE: src/Execution/CoercedValue.php
================================================
errors = $errors;
$this->value = $value;
}
/**
* @return GraphQLException[]
*/
public function getErrors(): array
{
return $this->errors;
}
/**
* @return bool
*/
public function hasErrors(): bool
{
return !empty($this->errors);
}
/**
* @param GraphQLException[] $errors
*/
public function setErrors(array $errors): void
{
$this->errors = $errors;
}
/**
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* @param mixed $value
*/
public function setValue($value): void
{
$this->value = $value;
}
}
================================================
FILE: src/Execution/Execution.php
================================================
createContext(
$schema,
$documentNode,
$rootValue,
$contextValue,
$variableValues,
$operationName,
$fieldResolver
);
// Return early errors if execution context failed.
if (!empty($context->getErrors())) {
return resolve(new ExecutionResult(null, $context->getErrors()));
}
} catch (ExecutionException $error) {
return resolve(new ExecutionResult(null, [$error]));
}
$fieldCollector = new FieldCollector($context);
$data = $this->executeOperation($operationName, $context, $fieldCollector);
if ($data instanceof PromiseInterface) {
return $data->then(function ($resolvedData) use ($context) {
return new ExecutionResult($resolvedData, $context->getErrors());
});
}
if (null !== $errorHandler) {
foreach ($context->getErrors() as $error) {
$errorHandler->handleExecutionError($error, $context);
}
}
return resolve(new ExecutionResult($data, $context->getErrors()));
}
/**
* @param null|string $operationName
* @param ExecutionContext $context
* @param FieldCollector $fieldCollector
* @return array|mixed|null|PromiseInterface
*/
protected function executeOperation(
?string $operationName,
ExecutionContext $context,
FieldCollector $fieldCollector
) {
$strategy = $operationName === 'mutation'
? new SerialExecutionStrategy($context, $fieldCollector)
: new ParallelExecutionStrategy($context, $fieldCollector);
$result = null;
try {
$result = $strategy->execute();
} catch (ExecutionException $exception) {
$context->addError($exception);
} catch (\Throwable $exception) {
$context->addError(
new ExecutionException($exception->getMessage(), null, null, null, null, null, $exception)
);
}
if ($result instanceof PromiseInterface) {
return $result->then(null, function (ExecutionException $exception) use ($context) {
$context->addError($exception);
return resolve(null);
});
}
return $result;
}
/**
* @param Schema $schema
* @param DocumentNode $documentNode
* @param mixed $rootValue
* @param mixed $contextValue
* @param mixed $rawVariableValues
* @param null|string $operationName
* @param callable|null $fieldResolver
* @return ExecutionContext
* @throws ExecutionException
*/
protected function createContext(
Schema $schema,
DocumentNode $documentNode,
$rootValue,
$contextValue,
$rawVariableValues,
?string $operationName = null,
?callable $fieldResolver = null
): ExecutionContext {
$errors = [];
$fragments = [];
$operation = null;
foreach ($documentNode->getDefinitions() as $definition) {
if ($definition instanceof OperationDefinitionNode) {
if (null === $operationName && null !== $operation) {
throw new ExecutionException(
'Must provide operation name if query contains multiple operations.'
);
}
if (null === $operationName || $definition->getNameValue() === $operationName) {
$operation = $definition;
}
continue;
}
if ($definition instanceof FragmentDefinitionNode || $definition instanceof FragmentSpreadNode) {
$fragments[$definition->getNameValue()] = $definition;
continue;
}
}
if (null === $operation) {
if (null !== $operationName) {
throw new ExecutionException(sprintf('Unknown operation named "%s".', $operationName));
}
throw new ExecutionException('Must provide an operation.');
}
$coercedVariableValues = ValuesResolver::coerceVariableValues(
$schema,
$operation->getVariableDefinitions(),
$rawVariableValues
);
$variableValues = $coercedVariableValues->getValue();
if ($coercedVariableValues->hasErrors()) {
$errors = $coercedVariableValues->getErrors();
}
return new ExecutionContext(
$schema,
$fragments,
$rootValue,
$contextValue,
$variableValues,
$fieldResolver,
$operation,
$errors
);
}
}
================================================
FILE: src/Execution/ExecutionContext.php
================================================
schema = $schema;
$this->fragments = $fragments;
$this->rootValue = $rootValue;
$this->contextValue = $contextValue;
$this->variableValues = $variableValues;
$this->fieldResolver = $fieldResolver;
$this->operation = $operation;
$this->errors = $errors;
}
/**
* @return mixed
*/
public function getRootValue()
{
return $this->rootValue;
}
/**
* @param mixed $rootValue
* @return ExecutionContext
*/
public function setRootValue($rootValue): ExecutionContext
{
$this->rootValue = $rootValue;
return $this;
}
/**
* @return mixed
*/
public function getContextValue()
{
return $this->contextValue ?? [];
}
/**
* @param mixed $contextValue
* @return ExecutionContext
*/
public function setContextValue($contextValue): ExecutionContext
{
$this->contextValue = $contextValue;
return $this;
}
/**
* @return mixed
*/
public function getVariableValues()
{
return $this->variableValues;
}
/**
* @param mixed $variableValues
* @return ExecutionContext
*/
public function setVariableValues($variableValues): ExecutionContext
{
$this->variableValues = $variableValues;
return $this;
}
/**
* @return bool
*/
public function hasFieldResolver(): bool
{
return null !== $this->fieldResolver;
}
/**
* @return callable|null
*/
public function getFieldResolver(): ?callable
{
return $this->fieldResolver;
}
/**
* @param callable|null $fieldResolver
* @return ExecutionContext
*/
public function setFieldResolver(?callable $fieldResolver): ExecutionContext
{
$this->fieldResolver = $fieldResolver;
return $this;
}
/**
* @return OperationDefinitionNode
*/
public function getOperation(): OperationDefinitionNode
{
return $this->operation;
}
/**
* @return Schema
*/
public function getSchema(): Schema
{
return $this->schema;
}
/**
* @return FragmentDefinitionNode[]
*/
public function getFragments(): array
{
return $this->fragments;
}
/**
* @param ExecutionException $error
* @return ExecutionContext
*/
public function addError(ExecutionException $error): ExecutionContext
{
$this->errors[] = $error;
return $this;
}
/**
* @return ExecutionException[]
*/
public function getErrors(): array
{
return $this->errors;
}
}
================================================
FILE: src/Execution/ExecutionException.php
================================================
container->share(ExecutionInterface::class, Execution::class);
}
}
================================================
FILE: src/Execution/ExecutionResult.php
================================================
data = $data;
$this->errors = $errors;
}
/**
* @return array|null
*/
public function getData(): ?array
{
return $this->data;
}
/**
* @return GraphQLException[]
*/
public function getErrors(): array
{
return $this->errors;
}
/**
* @param GraphQLException $error
* @return ExecutionResult
*/
public function addError(GraphQLException $error): ExecutionResult
{
$this->errors[] = $error;
return $this;
}
/**
* @inheritdoc
*/
public function toArray(): array
{
$array = [];
if (!empty($this->errors)) {
$array['errors'] = \array_map(function (GraphQLException $error) {
return $error->toArray();
}, $this->errors);
}
$array['data'] = $this->data;
return $array;
}
}
================================================
FILE: src/Execution/InvalidReturnTypeException.php
================================================
previous = $previous;
$this->key = $key;
}
/**
* @return Path|null
*/
public function getPrevious(): ?Path
{
return $this->previous;
}
/**
* @return string|mixed
*/
public function getKey()
{
return $this->key;
}
}
================================================
FILE: src/Execution/ResolveInfo.php
================================================
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;
}
/**
* @return string
*/
public function getFieldName(): string
{
return $this->fieldName;
}
/**
* @return FieldNode[]
*/
public function getFieldNodes(): array
{
return $this->fieldNodes;
}
/**
* @return TypeInterface
*/
public function getReturnType(): TypeInterface
{
return $this->returnType;
}
/**
* @return ObjectType
*/
public function getParentType(): ObjectType
{
return $this->parentType;
}
/**
* @return array
*/
public function getPath(): ?array
{
return $this->path;
}
/**
* @return Schema
*/
public function getSchema(): Schema
{
return $this->schema;
}
/**
* @return array
*/
public function getFragments(): array
{
return $this->fragments;
}
/**
* @return mixed
*/
public function getRootValue()
{
return $this->rootValue;
}
/**
* @return OperationDefinitionNode
*/
public function getOperation(): OperationDefinitionNode
{
return $this->operation;
}
/**
* @return array
*/
public function getVariableValues(): array
{
return $this->variableValues;
}
}
================================================
FILE: src/Execution/Strategy/AbstractExecutionStrategy.php
================================================
context = $context;
$this->fieldCollector = $fieldCollector;
$this->typeResolverCallback = $typeResolverCallback ?? [$this, 'defaultTypeResolver'];
$this->fieldResolverCallback = $fieldResolverCallback ?? [$this, 'defaultFieldResolver'];
}
/**
* @inheritdoc
* @throws ExecutionException
* @throws InvalidTypeException
* @throws InvariantException
* @throws ConversionException
* @throws \Throwable
*/
public function execute()
{
$schema = $this->context->getSchema();
$operation = $this->context->getOperation();
$rootValue = $this->context->getRootValue();
$objectType = $this->getOperationType($schema, $operation);
$fields = [];
$visitedFragmentNames = [];
$path = [];
$fields = $this->fieldCollector->collectFields(
$objectType,
$operation->getSelectionSet(),
$fields,
$visitedFragmentNames
);
// Errors from sub-fields of a NonNull type may propagate to the top level,
// at which point we still log the error and null the parent field, which
// in this case is the entire response.
try {
$result = $this->executeFields($objectType, $rootValue, $path, $fields);
} catch (ExecutionException $exception) {
$this->context->addError($exception);
return null;
} catch (\Throwable $exception) {
$exception = !$exception instanceof ExecutionException
? $this->normalizeException($exception, $fields, $path)
: $exception;
$this->context->addError($exception);
return null;
}
return $result;
}
/**
* @param Schema $schema
* @param OperationDefinitionNode $operation
*
* @return ObjectType|null
* @throws ExecutionException
*/
protected function getOperationType(Schema $schema, OperationDefinitionNode $operation): ?ObjectType
{
switch ($operation->getOperation()) {
case 'query':
return $schema->getQueryType();
case 'mutation':
$mutationType = $schema->getMutationType();
if (null === $mutationType) {
throw new ExecutionException('Schema is not configured for mutations.', [$operation]);
}
return $mutationType;
case 'subscription':
$subscriptionType = $schema->getSubscriptionType();
if (null === $subscriptionType) {
throw new ExecutionException('Schema is not configured for subscriptions.', [$operation]);
}
return $subscriptionType;
default:
throw new ExecutionException('Can only execute queries, mutations and subscriptions.', [$operation]);
}
}
/**
* 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.
*
* @param ObjectType $parentType
* @param mixed $rootValue
* @param FieldNode[] $fieldNodes
* @param string[] $path
*
* @return mixed
* @throws \Throwable
* @throws UndefinedFieldException
*/
protected function resolveField(ObjectType $parentType, $rootValue, array $fieldNodes, array $path)
{
$fieldNode = $fieldNodes[0];
$fieldName = $fieldNode->getNameValue();
$field = $this->getFieldDefinition($this->context->getSchema(), $parentType, $fieldName);
if (null === $field) {
throw new UndefinedFieldException($fieldName);
}
$info = $this->createResolveInfo($fieldNodes, $fieldNode, $field, $parentType, $path, $this->context);
$resolveCallback = $this->determineResolveCallback($field, $parentType);
$result = $this->resolveFieldValueOrError(
$field,
$fieldNode,
$resolveCallback,
$rootValue,
$this->context,
$info
);
$result = $this->completeValueCatchingError(
$field->getType(),
$fieldNodes,
$info,
$path,
$result
);
return $result;
}
/**
* @param Field $field
* @param FieldNode $fieldNode
* @param callable $resolveCallback
* @param mixed $rootValue
* @param ExecutionContext $context
* @param ResolveInfo $info
*
* @return array|\Throwable
*/
protected function resolveFieldValueOrError(
Field $field,
FieldNode $fieldNode,
?callable $resolveCallback,
$rootValue,
ExecutionContext $context,
ResolveInfo $info
) {
try {
// Build an associative array of arguments from the field.arguments AST, using the
// variables scope to fulfill any variable references.
$result = $resolveCallback(
$rootValue,
ValuesResolver::coerceArgumentValues($field, $fieldNode, $context->getVariableValues()),
$context->getContextValue(),
$info
);
if ($result instanceof PromiseInterface) {
return $result->then(null, function ($exception) use ($fieldNode, $info) {
return !$exception instanceof ExecutionException
? $this->normalizeException($exception, [$fieldNode], $info->getPath())
: $exception;
});
}
return $result;
} catch (\Throwable $exception) {
return $exception;
}
}
/**
* Normalizes exceptions which are usually a \Throwable,
* but can even be a string or null when resolving promises.
*
* @param mixed $exception
* @param NodeInterface[] $nodes
* @param array $path
* @return ExecutionException
*/
protected function normalizeException($exception, array $nodes, array $path = []): ExecutionException
{
if ($exception instanceof \Throwable) {
return new ExecutionException(
$exception->getMessage(),
$nodes,
null,
null,
$path,
null,
$exception
);
}
if (\is_string($exception)) {
return new ExecutionException(
$exception,
$nodes,
null,
null,
$path
);
}
return new ExecutionException(
'',
$nodes,
null,
null,
$path
);
}
/**
* This is a small wrapper around completeValue which detects and logs error in the execution context.
*
* @param TypeInterface $returnType
* @param FieldNode[] $fieldNodes
* @param ResolveInfo $info
* @param array $path
* @param mixed $result
*
* @return array|null|PromiseInterface
* @throws \Throwable
*/
public function completeValueCatchingError(
TypeInterface $returnType,
array $fieldNodes,
ResolveInfo $info,
array $path,
&$result
) {
try {
$completed = $result instanceof PromiseInterface
? $result->then(function ($resolvedResult) use ($returnType, $fieldNodes, $info, $path) {
return $this->completeValue($returnType, $fieldNodes, $info, $path, $resolvedResult);
})
: $this->completeValue($returnType, $fieldNodes, $info, $path, $result);
if ($completed instanceof PromiseInterface) {
// Note: we don't rely on a `catch` method, but we do expect "thenable"
// to take a second callback for the error case.
return $completed->then(null, function ($exception) use ($fieldNodes, $path, $returnType) {
$this->handleFieldError($exception, $fieldNodes, $path, $returnType);
});
}
return $completed;
} catch (\Throwable $exception) {
$this->handleFieldError($exception, $fieldNodes, $path, $returnType);
return null;
}
}
/**
* Implements the instructions for completeValue as defined in the
* "Field entries" section of the spec.
*
* If the field type is Non-Null, then this recursively completes the value
* for the inner type. It throws a field error if that completion returns null,
* as per the "Nullability" section of the spec.
*
* If the field type is a List, then this recursively completes the value
* for the inner type on each item in the list.
*
* If the field type is a Scalar or Enum, ensures the completed value is a legal
* value of the type by calling the `serialize` method of GraphQL type
* definition.
*
* If the field is an abstract type, determine the runtime type of the value
* and then complete based on that type
*
* Otherwise, the field type expects a sub-selection set, and will complete the
* value by evaluating all sub-selections.
*
* @param TypeInterface $returnType
* @param FieldNode[] $fieldNodes
* @param ResolveInfo $info
* @param array $path
* @param mixed $result
*
* @return array|null|PromiseInterface
* @throws \Throwable
*/
protected function completeValue(
TypeInterface $returnType,
array $fieldNodes,
ResolveInfo $info,
array $path,
&$result
) {
// 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 NonNullType) {
$completed = $this->completeValue(
$returnType->getOfType(),
$fieldNodes,
$info,
$path,
$result
);
if (null !== $completed) {
return $completed;
}
throw new ExecutionException(
\sprintf(
'Cannot return null for non-nullable field %s.%s.',
(string)$info->getParentType(),
$info->getFieldName()
)
);
}
// If result is null, return null.
if (null === $result) {
return null;
}
// If field type is a leaf type, Scalar or Enum, serialize to a valid value,
// returning null if serialization is not possible.
if ($returnType instanceof ListType) {
return $this->completeListValue($returnType, $fieldNodes, $info, $path, $result);
}
// If field type is Scalar or Enum, serialize to a valid value, returning
// null if serialization is not possible.
if ($returnType instanceof LeafTypeInterface) {
return $this->completeLeafValue($returnType, $result);
}
// If field type is an abstract type, Interface or Union, determine the
// runtime Object type and complete for that type.
if ($returnType instanceof AbstractTypeInterface) {
return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $result);
}
// If field type is Object, execute and complete all sub-selections.
if ($returnType instanceof ObjectType) {
return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $result);
}
throw new ExecutionException(\sprintf('Cannot complete value of unexpected type "%s".', (string)$returnType));
}
/**
* @param ListType $returnType
* @param FieldNode[] $fieldNodes
* @param ResolveInfo $info
* @param array $path
* @param mixed $result
*
* @return array|PromiseInterface
* @throws \Throwable
*/
protected function completeListValue(
ListType $returnType,
array $fieldNodes,
ResolveInfo $info,
array $path,
&$result
) {
if (!\is_array($result) && !$result instanceof \Traversable) {
throw new InvariantException(
\sprintf(
'Expected Array or Traversable, but did not find one for field %s.%s.',
(string)$info->getParentType(),
$info->getFieldName()
)
);
}
$itemType = $returnType->getOfType();
$completedItems = [];
$containsPromise = false;
foreach ($result as $key => $item) {
$fieldPath = $path;
$fieldPath[] = $key;
$completedItem = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $fieldPath, $item);
$completedItems[] = $completedItem;
$containsPromise = $containsPromise || $completedItem instanceof PromiseInterface;
}
return $containsPromise
? \React\Promise\all($completedItems)
: $completedItems;
}
/**
* @param LeafTypeInterface|SerializableTypeInterface $returnType
* @param mixed $result
*
* @return array|PromiseInterface
* @throws InvalidReturnTypeException
*/
protected function completeLeafValue($returnType, &$result)
{
$result = $returnType->serialize($result);
if (null === $result) {
throw new InvalidReturnTypeException($returnType, $result);
}
return $result;
}
/**
* @param AbstractTypeInterface $returnType
* @param FieldNode[] $fieldNodes
* @param ResolveInfo $info
* @param string[] $path
* @param mixed $result
*
* @return array|PromiseInterface
* @throws \Throwable
*/
protected function completeAbstractValue(
AbstractTypeInterface $returnType,
array $fieldNodes,
ResolveInfo $info,
array $path,
&$result
) {
$runtimeType = $returnType->hasResolveTypeCallback()
? $returnType->resolveType($result, $this->context->getContextValue(), $info)
: \call_user_func(
$this->typeResolverCallback,
$result,
$this->context->getContextValue(),
$info,
$returnType
);
if ($runtimeType instanceof PromiseInterface) {
return $runtimeType->then(function ($resolvedRuntimeType) use (
$returnType,
$fieldNodes,
$info,
$path,
&$result
) {
return $this->completeObjectValue(
$this->ensureValidRuntimeType($resolvedRuntimeType, $returnType, $info, $result),
$fieldNodes,
$info,
$path,
$result
);
});
}
$returnType = $this->ensureValidRuntimeType($runtimeType, $returnType, $info, $result);
return $this->completeObjectValue(
$returnType,
$fieldNodes,
$info,
$path,
$result
);
}
/**
* @param ObjectType $returnType
* @param array $fieldNodes
* @param ResolveInfo $info
* @param array $path
* @param mixed $result
*
* @return array|PromiseInterface
* @throws ExecutionException
* @throws InvalidReturnTypeException
* @throws \Throwable
*/
protected function completeObjectValue(
ObjectType $returnType,
array $fieldNodes,
ResolveInfo $info,
array $path,
&$result
) {
// If there is an isTypeOfCallback predicate function, call it with the
// current result. If isTypeOfCallback returns false, then raise an error rather
// than continuing execution.
if ($returnType->hasIsTypeOfCallback()) {
$isTypeOf = $returnType->isTypeOf($result, $this->context->getContextValue(), $info);
if ($isTypeOf instanceof PromiseInterface) {
return $isTypeOf->then(function ($resolvedIsTypeOf) use ($returnType, $result, $fieldNodes, $path) {
if (true === $resolvedIsTypeOf) {
return $this->executeSubFields($returnType, $fieldNodes, $path, $result);
}
throw new InvalidReturnTypeException($returnType, $result, $fieldNodes);
});
}
if (false === $isTypeOf) {
throw new InvalidReturnTypeException($returnType, $result, $fieldNodes);
}
}
return $this->executeSubFields($returnType, $fieldNodes, $path, $result);
}
/**
* @param ObjectType $returnType
* @param FieldNode[] $fieldNodes
* @param array $path
* @param mixed $result
*
* @return array|PromiseInterface
* @throws \Throwable
*/
protected function executeSubFields(ObjectType $returnType, array $fieldNodes, array $path, &$result)
{
$subFields = [];
$visitedFragmentNames = [];
foreach ($fieldNodes as $fieldNode) {
if (null !== $fieldNode->getSelectionSet()) {
$subFields = $this->fieldCollector->collectFields(
$returnType,
$fieldNode->getSelectionSet(),
$subFields,
$visitedFragmentNames
);
}
}
if (!empty($subFields)) {
return $this->executeFields($returnType, $result, $path, $subFields);
}
return $result;
}
/**
* @param NamedTypeInterface|string $runtimeTypeOrName
* @param NamedTypeInterface $returnType
* @param ResolveInfo $info
* @param mixed $result
*
* @return TypeInterface|ObjectType|null
* @throws ExecutionException
* @throws InvariantException
*/
protected function ensureValidRuntimeType(
$runtimeTypeOrName,
NamedTypeInterface $returnType,
ResolveInfo $info,
&$result
) {
/** @var NamedTypeInterface $runtimeType */
$runtimeType = \is_string($runtimeTypeOrName)
? $this->context->getSchema()->getType($runtimeTypeOrName)
: $runtimeTypeOrName;
$runtimeTypeName = $runtimeType->getName();
$returnTypeName = $returnType->getName();
if (!$runtimeType instanceof ObjectType) {
$parentTypeName = $info->getParentType()->getName();
$fieldName = $info->getFieldName();
throw new ExecutionException(
\sprintf(
'Abstract type %s must resolve to an Object type at runtime for field %s.%s ' .
'with value "%s", received "%s".',
$returnTypeName,
$parentTypeName,
$fieldName,
$result,
$runtimeTypeName
)
);
}
if (!$this->context->getSchema()->isPossibleType($returnType, $runtimeType)) {
throw new ExecutionException(
\sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeTypeName, $returnTypeName)
);
}
if ($runtimeType !== $this->context->getSchema()->getType($runtimeType->getName())) {
throw new ExecutionException(
\sprintf(
'Schema must contain unique named types but contains multiple types named "%s". ' .
'Make sure that `resolveType` function of abstract type "%s" returns the same ' .
'type instance as referenced anywhere else within the schema.',
$runtimeTypeName,
$returnTypeName
)
);
}
return $runtimeType;
}
/**
* @param Schema $schema
* @param ObjectType $parentType
* @param string $fieldName
*
* @return Field|null
* @throws InvariantException
*/
public function getFieldDefinition(Schema $schema, ObjectType $parentType, string $fieldName): ?Field
{
if ($fieldName === '__schema' && $schema->getQueryType() === $parentType) {
return SchemaMetaFieldDefinition();
}
if ($fieldName === '__type' && $schema->getQueryType() === $parentType) {
return TypeMetaFieldDefinition();
}
if ($fieldName === '__typename') {
return TypeNameMetaFieldDefinition();
}
$fields = $parentType->getFields();
return $fields[$fieldName] ?? null;
}
/**
* @param Field $field
* @param ObjectType $parentType
*
* @return callable
*/
protected function determineResolveCallback(Field $field, ObjectType $parentType): callable
{
if ($field->hasResolveCallback()) {
return $field->getResolveCallback();
}
if ($parentType->hasResolveCallback()) {
return $parentType->getResolveCallback();
}
if ($this->context->hasFieldResolver()) {
return $this->context->getFieldResolver();
}
return $this->fieldResolverCallback;
}
/**
* @param \Throwable $originalException
* @param array $fieldNodes
* @param array $path
* @param TypeInterface $returnType
* @throws ExecutionException
*/
protected function handleFieldError(
\Throwable $originalException,
array $fieldNodes,
array $path,
TypeInterface $returnType
): void {
$exception = $this->buildLocatedError($originalException, $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 NonNullType) {
throw $exception;
}
// Otherwise, error protection is applied, logging the error and resolving
// a null value for this field if one is encountered.
$this->context->addError($exception);
}
/**
* @param \Throwable $originalException
* @param NodeInterface[] $nodes
* @param string[] $path
*
* @return ExecutionException
*/
protected function buildLocatedError(
\Throwable $originalException,
array $nodes = [],
array $path = []
): ExecutionException {
return new ExecutionException(
$originalException->getMessage(),
$originalException instanceof GraphQLException
? $originalException->getNodes()
: $nodes,
$originalException instanceof GraphQLException
? $originalException->getSource()
: null,
$originalException instanceof GraphQLException
? $originalException->getPositions()
: null,
$originalException instanceof GraphQLException
? ($originalException->getPath() ?? $path)
: $path,
null,
$originalException
);
}
/**
* @param FieldNode[] $fieldNodes
* @param FieldNode $fieldNode
* @param Field $field
* @param ObjectType $parentType
* @param array|null $path
* @param ExecutionContext $context
*
* @return ResolveInfo
*/
protected function createResolveInfo(
array $fieldNodes,
FieldNode $fieldNode,
Field $field,
ObjectType $parentType,
?array $path,
ExecutionContext $context
): ResolveInfo {
return new ResolveInfo(
$fieldNode->getNameValue(),
$fieldNodes,
$field->getType(),
$parentType,
$path,
$context->getSchema(),
$context->getFragments(),
$context->getRootValue(),
$context->getOperation(),
$context->getVariableValues()
);
}
/**
* @param mixed $value
* @param mixed $context
* @param ResolveInfo $info
* @param AbstractTypeInterface $abstractType
*
* @return mixed
* @throws InvariantException
*/
public static function defaultTypeResolver(
$value,
$context,
ResolveInfo $info,
AbstractTypeInterface $abstractType
) {
// First, look for `__typename`.
if (\is_array($value) && isset($value['__typename'])) {
return $value['__typename'];
}
// Otherwise, test each possible type.
/** @var ObjectType[] $possibleTypes */
$possibleTypes = $info->getSchema()->getPossibleTypes($abstractType);
$promises = [];
foreach ($possibleTypes as $index => $type) {
$isTypeOf = $type->isTypeOf($value, $context, $info);
if ($isTypeOf instanceof PromiseInterface) {
$promises[$index] = $isTypeOf;
}
if (true === $isTypeOf) {
return $type;
}
}
if (!empty($promises)) {
return \React\Promise\all($promises)->then(function ($resolvedPromises) use ($possibleTypes) {
foreach ($resolvedPromises as $index => $result) {
if (true === $result) {
return $possibleTypes[$index];
}
}
return null;
});
}
return null;
}
/**
* Try to resolve a field without any field resolver function.
*
* @param array|object $rootValue
* @param array $arguments
* @param mixed $contextValues
* @param ResolveInfo $info
*
* @return mixed|null
*/
public static function defaultFieldResolver($rootValue, array $arguments, $contextValues, ResolveInfo $info)
{
$fieldName = $info->getFieldName();
$property = null;
if (\is_array($rootValue) && isset($rootValue[$fieldName])) {
$property = $rootValue[$fieldName];
}
if (\is_object($rootValue)) {
$getter = 'get' . \ucfirst($fieldName);
if (\method_exists($rootValue, $getter)) {
$property = $rootValue->{$getter}();
} elseif (\method_exists($rootValue, $fieldName)) {
$property = $rootValue->{$fieldName}($rootValue, $arguments, $contextValues, $info);
} elseif (\property_exists($rootValue, $fieldName)) {
$property = $rootValue->{$fieldName};
}
}
return $property instanceof \Closure
? $property($rootValue, $arguments, $contextValues, $info)
: $property;
}
}
================================================
FILE: src/Execution/Strategy/ExecutionStrategyInterface.php
================================================
context = $context;
$this->skipDirective = SkipDirective();
$this->includeDirective = IncludeDirective();
}
/**
* @param ObjectType $runtimeType
* @param SelectionSetNode $selectionSet
* @param array $fields
* @param array $visitedFragmentNames
* @return array
* @throws InvalidTypeException
* @throws ExecutionException
* @throws InvariantException
* @throws ConversionException
*/
public function collectFields(
ObjectType $runtimeType,
SelectionSetNode $selectionSet,
array &$fields,
array &$visitedFragmentNames
): array {
foreach ($selectionSet->getSelections() as $selection) {
// Check if this Node should be included first
if (!$this->shouldIncludeNode($selection)) {
continue;
}
// Collect fields
if ($selection instanceof FieldNode) {
$fieldName = $selection->getAliasOrNameValue();
if (!isset($fields[$fieldName])) {
$fields[$fieldName] = [];
}
$fields[$fieldName][] = $selection;
continue;
}
if ($selection instanceof InlineFragmentNode) {
if (!$this->doesFragmentConditionMatch($selection, $runtimeType)) {
continue;
}
$this->collectFields($runtimeType, $selection->getSelectionSet(), $fields, $visitedFragmentNames);
continue;
}
if ($selection instanceof FragmentSpreadNode) {
$fragmentName = $selection->getNameValue();
if (isset($visitedFragmentNames[$fragmentName])) {
continue;
}
$visitedFragmentNames[$fragmentName] = true;
$fragment = $this->context->getFragments()[$fragmentName];
$this->collectFields($runtimeType, $fragment->getSelectionSet(), $fields, $visitedFragmentNames);
continue;
}
}
return $fields;
}
/**
* @param NodeInterface $node
* @return bool
* @throws ExecutionException
* @throws InvalidTypeException
* @throws InvariantException
*/
protected function shouldIncludeNode(NodeInterface $node): bool
{
$contextVariables = $this->context->getVariableValues();
$skip = ValuesResolver::coerceDirectiveValues($this->skipDirective, $node, $contextVariables);
if ($skip && $skip['if'] === true) {
return false;
}
$include = ValuesResolver::coerceDirectiveValues($this->includeDirective, $node, $contextVariables);
if ($include && $include['if'] === false) {
return false;
}
return true;
}
/**
* @param FragmentDefinitionNode|InlineFragmentNode $fragment
* @param ObjectType $type
* @return bool
* @throws InvariantException
* @throws ConversionException
*/
protected function doesFragmentConditionMatch($fragment, ObjectType $type): bool
{
$typeConditionNode = $fragment->getTypeCondition();
if (null === $typeConditionNode) {
return true;
}
$conditionalType = TypeASTConverter::convert($this->context->getSchema(), $typeConditionNode);
if ($type === $conditionalType) {
return true;
}
if ($conditionalType instanceof AbstractTypeInterface) {
return $this->context->getSchema()->isPossibleType($conditionalType, $type);
}
return false;
}
}
================================================
FILE: src/Execution/Strategy/ParallelExecutionStrategy.php
================================================
$fieldNodes) {
$fieldPath = $path;
$fieldPath[] = $fieldName;
try {
$result = $this->resolveField($parentType, $rootValue, $fieldNodes, $fieldPath);
} catch (UndefinedFieldException $exception) {
continue;
}
$containsPromise = $containsPromise || $result instanceof PromiseInterface;
$results[$fieldName] = $result;
}
if (!$containsPromise) {
return $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 promiseForMap($results);
}
}
================================================
FILE: src/Execution/Strategy/SerialExecutionStrategy.php
================================================
resolveField($parentType, $rootValue, $fieldNodes, $fieldPath);
} catch (UndefinedFieldException $exception) {
return null;
}
if ($result instanceof PromiseInterface) {
return $result->then(function ($resolvedResult) use ($fieldName, $results) {
$results[$fieldName] = $resolvedResult;
return $results;
});
}
$results[$fieldName] = $result;
return $results;
}
);
}
}
================================================
FILE: src/Execution/UndefinedFieldException.php
================================================
getArguments();
$argumentNodes = $node->getArguments();
if (empty($argumentDefinitions)) {
return $coercedValues;
}
/** @var ArgumentNode[] $argumentNodeMap */
$argumentNodeMap = keyMap($argumentNodes, function (ArgumentNode $value) {
return $value->getNameValue();
});
foreach ($argumentDefinitions as $argumentDefinition) {
$argumentName = $argumentDefinition->getName();
$argumentType = $argumentDefinition->getType();
$argumentNode = $argumentNodeMap[$argumentName] ?? null;
$defaultValue = $argumentDefinition->getDefaultValue();
$argumentValue = null !== $argumentNode ? $argumentNode->getValue() : null;
if (null !== $argumentNode && $argumentValue instanceof VariableNode) {
$variableName = $argumentValue->getNameValue();
$hasValue = !empty($variableValues) && \array_key_exists($variableName, $variableValues);
$isNull = $hasValue && null === $variableValues[$variableName];
} else {
$hasValue = null !== $argumentNode;
$isNull = $hasValue && $argumentValue instanceof NullValueNode;
}
if (!$hasValue && null !== $defaultValue) {
// If no argument was provided where the definition has a default value,
// use the default value.
$coercedValues[$argumentName] = $defaultValue;
} elseif ((!$hasValue || $isNull) && $argumentType instanceof NonNullType) {
// 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 ExecutionException(
\sprintf(
'Argument "%s" of non-null type "%s" must not be null.',
$argumentName,
$argumentType
),
[$argumentValue]
);
} elseif (null !== $argumentNode && $argumentValue instanceof VariableNode) {
$variableName = $argumentValue->getNameValue();
throw new ExecutionException(
\sprintf(
'Argument "%s" of required type "%s" was provided the variable "$%s" '
. 'which was not provided a runtime value.',
$argumentName,
$argumentType,
$variableName
),
[$argumentValue]
);
} else {
throw new ExecutionException(
\sprintf(
'Argument "%s" of required type "%s" was not provided.',
$argumentName,
$argumentType
),
[$node]
);
}
} elseif ($hasValue) {
if ($argumentValue instanceof NullValueNode) {
// If the explicit value `null` was provided, an entry in the coerced
// values must exist as the value `null`.
$coercedValues[$argumentName] = null;
} elseif ($argumentValue instanceof VariableNode) {
$variableName = $argumentValue->getNameValue();
invariant(!empty($variableValues), '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[$argumentName] = $variableValues[$variableName];
} else {
$valueNode = $argumentNode->getValue();
try {
// Value nodes that cannot be resolved should be treated as invalid values
// because there is no undefined value in PHP so that we throw an exception
$coercedValue = ValueASTConverter::convert($valueNode, $argumentType, $variableValues);
} catch (\Exception $ex) {
// 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 ExecutionException(
\sprintf(
'Argument "%s" has invalid value %s.',
$argumentName,
(string)$argumentValue
),
[$argumentValue],
null,
null,
null,
null,
$ex
);
}
$coercedValues[$argumentName] = $coercedValue;
}
}
}
return $coercedValues;
}
/**
* Prepares an object map of argument values given a directive definition
* and a AST node which may contain directives. Optionally also accepts a map
* of variable values.
*
* If the directive does not exist on the node, returns null.
*
* @param Directive $directive
* @param mixed $node
* @param array $variableValues
* @return array|null
* @throws ExecutionException
* @throws InvariantException
*/
public static function coerceDirectiveValues(
Directive $directive,
$node,
array $variableValues = []
): ?array {
$directiveNode = $node->hasDirectives()
? find($node->getDirectives(), function (NameAwareInterface $value) use ($directive) {
return $value->getNameValue() === $directive->getName();
}) : null;
if (null !== $directiveNode) {
return static::coerceArgumentValues($directive, $directiveNode, $variableValues);
}
return null;
}
/**
* Prepares an object map of variableValues of the correct type based on the
* provided variable definitions and arbitrary input. If the input cannot be
* parsed to match the variable definitions, a GraphQLError will be thrown.
*
* @param Schema $schema
* @param array|VariableDefinitionNode[] $variableDefinitionNodes
* @param array $inputs
* @return CoercedValue
* @throws GraphQLException
* @throws InvariantException
* @throws ConversionException
*/
public static function coerceVariableValues(
Schema $schema,
array $variableDefinitionNodes,
array $inputs
): CoercedValue {
$coercedValues = [];
$errors = [];
foreach ($variableDefinitionNodes as $variableDefinitionNode) {
$variableName = $variableDefinitionNode->getVariable()->getNameValue();
$variableType = TypeASTConverter::convert($schema, $variableDefinitionNode->getType());
$variableTypeName = (string)$variableType;
if ($variableTypeName === '') {
$variableTypeName = (string)$variableDefinitionNode;
}
if (!static::isInputType($variableType)) {
// Must use input types for variables. This should be caught during
// validation, however is checked again here for safety.
$errors[] = static::buildCoerceException(
\sprintf(
'Variable "$%s" expected value of type "%s" which cannot be used as an input type',
$variableName,
$variableTypeName
),
$variableDefinitionNode,
null
);
} else {
$hasValue = \array_key_exists($variableName, $inputs);
$value = $hasValue ? $inputs[$variableName] : null;
if (!$hasValue && $variableDefinitionNode->hasDefaultValue()) {
// If no value was provided to a variable with a default value,
// use the default value.
$coercedValues[$variableName] = ValueASTConverter::convert(
$variableDefinitionNode->getDefaultValue(),
$variableType
);
} elseif ((!$hasValue || null === $value) && $variableType instanceof NonNullType) {
// If no value or a nullish value was provided to a variable with a
// non-null type (required), produce an error.
$errors[] = static::buildCoerceException(
\sprintf(
$value
? 'Variable "$%s" of non-null type "%s" must not be null'
: 'Variable "$%s" of required type "%s" was not provided',
$variableName,
$variableTypeName
),
$variableDefinitionNode,
null
);
} elseif ($hasValue) {
if (null === $value) {
// If the explicit value `null` was provided, an entry in the coerced
// values must exist as the value `null`.
$coercedValues[$variableName] = null;
} else {
// Otherwise, a non-null value was provided, coerce it to the expected
// type or report an error if coercion fails.
$coercedValue = static::coerceValue($value, $variableType, $variableDefinitionNode);
if ($coercedValue->hasErrors()) {
$message = \sprintf(
'Variable "$%s" got invalid value %s',
$variableName,
jsonEncode($value)
);
foreach ($coercedValue->getErrors() as $error) {
$errors[] = static::buildCoerceException(
$message,
$variableDefinitionNode,
null,
$error->getMessage(),
$error
);
}
} else {
$coercedValues[$variableName] = $coercedValue->getValue();
}
}
}
}
}
return new CoercedValue($coercedValues, $errors);
}
/**
* @param TypeInterface|null $type
* @return bool
*/
protected static function isInputType(?TypeInterface $type)
{
return ($type instanceof ScalarType) ||
($type instanceof EnumType) ||
($type instanceof InputObjectType) ||
(($type instanceof WrappingTypeInterface) && static::isInputType($type->getOfType()));
}
/**
* Returns either a value which is valid for the provided type or a list of
* encountered coercion errors.
*
* @param mixed|array $value
* @param mixed $type
* @param NodeInterface $blameNode
* @param Path|null $path
* @return CoercedValue
* @throws GraphQLException
* @throws InvariantException
*/
protected static function coerceValue($value, $type, $blameNode, ?Path $path = null): CoercedValue
{
if ($type instanceof NonNullType) {
return static::coerceValueForNonNullType($value, $type, $blameNode, $path);
}
if (null === $value) {
return new CoercedValue(null);
}
if ($type instanceof ScalarType) {
return static::coerceValueForScalarType($value, $type, $blameNode, $path);
}
if ($type instanceof EnumType) {
return static::coerceValueForEnumType($value, $type, $blameNode, $path);
}
if ($type instanceof ListType) {
return static::coerceValueForListType($value, $type, $blameNode, $path);
}
if ($type instanceof InputObjectType) {
return static::coerceValueForInputObjectType($value, $type, $blameNode, $path);
}
throw new GraphQLException('Unexpected type.');
}
/**
* @param mixed $value
* @param NonNullType $type
* @param NodeInterface $blameNode
* @param Path|null $path
* @return CoercedValue
* @throws GraphQLException
* @throws InvariantException
*/
protected static function coerceValueForNonNullType(
$value,
NonNullType $type,
NodeInterface $blameNode,
?Path $path
): CoercedValue {
if (null === $value) {
return new CoercedValue(null, [
static::buildCoerceException(
\sprintf('Expected non-nullable type %s not to be null', (string)$type),
$blameNode,
$path
)
]);
}
return static::coerceValue($value, $type->getOfType(), $blameNode, $path);
}
/**
* Scalars determine if a value is valid via parseValue(), which can
* throw to indicate failure. If it throws, maintain a reference to
* the original error.
*
* @param mixed $value
* @param ScalarType $type
* @param NodeInterface $blameNode
* @param Path|null $path
* @return CoercedValue
*/
protected static function coerceValueForScalarType(
$value,
ScalarType $type,
NodeInterface $blameNode,
?Path $path
): CoercedValue {
try {
$parseResult = $type->parseValue($value);
if (null === $parseResult) {
return new CoercedValue(null, [
new GraphQLException(\sprintf('Expected type %s', (string)$type))
]);
}
return new CoercedValue($parseResult);
} /** @noinspection PhpRedundantCatchClauseInspection */ catch (InvalidTypeException|CoercingException $ex) {
return new CoercedValue(null, [
static::buildCoerceException(
\sprintf('Expected type %s', (string)$type),
$blameNode,
$path,
$ex->getMessage(),
$ex
)
]);
}
}
/**
* @param mixed $value
* @param EnumType $type
* @param NodeInterface $blameNode
* @param Path|null $path
* @return CoercedValue
* @throws InvariantException
*/
protected static function coerceValueForEnumType(
$value,
EnumType $type,
NodeInterface $blameNode,
?Path $path
): CoercedValue {
if (\is_string($value) && null !== ($enumValue = $type->getValue($value))) {
return new CoercedValue($enumValue);
}
$suggestions = suggestionList((string)$value, \array_map(function (EnumValue $enumValue) {
return $enumValue->getName();
}, $type->getValues()));
$didYouMean = (!empty($suggestions))
? 'did you mean' . \implode(',', $suggestions)
: null;
return new CoercedValue(null, [
static::buildCoerceException(\sprintf('Expected type %s', $type->getName()), $blameNode, $path, $didYouMean)
]);
}
/**
* @param mixed $value
* @param InputObjectType $type
* @param NodeInterface $blameNode
* @param Path|null $path
* @return CoercedValue
* @throws InvariantException
* @throws GraphQLException
*/
protected static function coerceValueForInputObjectType(
$value,
InputObjectType $type,
NodeInterface $blameNode,
?Path $path
): CoercedValue {
$errors = [];
$coercedValues = [];
$fields = $type->getFields();
// Ensure every defined field is valid.
foreach ($fields as $field) {
$fieldType = $field->getType();
if (!isset($value[$field->getName()])) {
if (!empty($field->getDefaultValue())) {
$coercedValue[$field->getName()] = $field->getDefaultValue();
} elseif ($fieldType instanceof NonNullType) {
$errors[] = new GraphQLException(
\sprintf(
"Field %s of required type %s! was not provided.",
static::printPath(new Path($path, $field->getName())),
(string)$fieldType->getOfType()
)
);
}
} else {
$fieldValue = $value[$field->getName()];
$coercedValue = static::coerceValue(
$fieldValue,
$fieldType,
$blameNode,
new Path($path, $field->getName())
);
if ($coercedValue->hasErrors()) {
$errors = \array_merge($errors, $coercedValue->getErrors());
} elseif (empty($errors)) {
$coercedValues[$field->getName()] = $coercedValue->getValue();
}
}
}
// Ensure every provided field is defined.
foreach ($value as $fieldName => $fieldValue) {
if (!isset($fields[$fieldName])) {
$suggestions = suggestionList($fieldName, \array_keys($fields));
$didYouMean = (!empty($suggestions))
? 'did you mean' . \implode(',', $suggestions)
: null;
$errors[] = static::buildCoerceException(
\sprintf('Field "%s" is not defined by type %s', $fieldName, $type->getName()),
$blameNode,
$path,
$didYouMean
);
}
}
return new CoercedValue($coercedValues, $errors);
}
/**
* @param mixed $value
* @param ListType $type
* @param NodeInterface $blameNode
* @param Path|null $path
* @return CoercedValue
* @throws GraphQLException
* @throws InvariantException
*/
protected static function coerceValueForListType(
$value,
ListType $type,
NodeInterface $blameNode,
?Path $path
): CoercedValue {
$itemType = $type->getOfType();
if (\is_array($value) || $value instanceof \Traversable) {
$errors = [];
$coercedValues = [];
foreach ($value as $index => $itemValue) {
$coercedValue = static::coerceValue($itemValue, $itemType, $blameNode, new Path($path, $index));
if ($coercedValue->hasErrors()) {
$errors = \array_merge($errors, $coercedValue->getErrors());
} else {
$coercedValues[] = $coercedValue->getValue();
}
}
return new CoercedValue($coercedValues, $errors);
}
// Lists accept a non-list value as a list of one.
$coercedValue = static::coerceValue($value, $itemType, $blameNode);
return new CoercedValue([$coercedValue->getValue()], $coercedValue->getErrors());
}
/**
* @param string $message
* @param NodeInterface $blameNode
* @param Path|null $path
* @param null|string $subMessage
* @param GraphQLException|null $originalException
* @return GraphQLException
*/
protected static function buildCoerceException(
string $message,
NodeInterface $blameNode,
?Path $path,
?string $subMessage = null,
?GraphQLException $originalException = null
) {
$stringPath = static::printPath($path);
return new CoercingException(
$message .
(($stringPath !== '') ? ' at ' . $stringPath : $stringPath) .
(($subMessage !== null) ? '; ' . $subMessage : '.'),
[$blameNode],
null,
null,
null,
null,
$originalException
);
}
/**
* @param Path|null $path
* @return string
*/
protected static function printPath(?Path $path)
{
$stringPath = '';
$currentPath = $path;
while ($currentPath !== null) {
$stringPath = \is_string($currentPath->getKey())
? '.' . $currentPath->getKey() . $stringPath
: '[' . (string)$currentPath->getKey() . ']' . $stringPath;
$currentPath = $currentPath->getPrevious();
}
return !empty($stringPath) ? 'value' . $stringPath : '';
}
}
================================================
FILE: src/GraphQL.php
================================================
registerProviders($container);
$this->container = $container;
}
/**
* @return GraphQL
*/
public static function getInstance(): self
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* @param string $id
* @return mixed
*/
public static function make(string $id)
{
return static::getInstance()
->getContainer()
->get($id);
}
/**
* @param Source $source
* @param array|ResolverRegistryInterface $resolverRegistry
* @param array $options
* @return Schema
*/
public static function buildSchema(Source $source, $resolverRegistry, array $options = []): Schema
{
/** @var SchemaBuilderInterface $schemaBuilder */
$schemaBuilder = static::make(SchemaBuilderInterface::class);
return $schemaBuilder->build(
static::parse($source, $options),
$resolverRegistry instanceof ResolverRegistryInterface
? $resolverRegistry
: new ResolverRegistry($resolverRegistry),
$options
);
}
/**
* @param Schema $schema
* @param Source $source
* @param array|ResolverRegistryInterface $resolverRegistry
* @param array $options
* @return Schema
*/
public static function extendSchema(
Schema $schema,
Source $source,
$resolverRegistry,
array $options = []
): Schema {
/** @var SchemaExtenderInterface $schemaExtender */
$schemaExtender = static::make(SchemaExtenderInterface::class);
return $schemaExtender->extend(
$schema,
static::parse($source, $options),
$resolverRegistry instanceof ResolverRegistryInterface
? $resolverRegistry
: new ResolverRegistry($resolverRegistry),
$options
);
}
/**
* @param Schema $schema
* @return array
*/
public static function validateSchema(Schema $schema): array
{
/** @var SchemaValidatorInterface $schemaValidator */
$schemaValidator = static::make(SchemaValidatorInterface::class);
return $schemaValidator->validate($schema);
}
/**
* @param Source $source
* @param array $options
* @return DocumentNode
*/
public static function parse(Source $source, array $options = []): DocumentNode
{
/** @var ParserInterface $parser */
$parser = static::make(ParserInterface::class);
return $parser->parse($source, $options);
}
/**
* @param Source $source
* @param array $options
* @return ValueNodeInterface
*/
public static function parseValue(Source $source, array $options = []): ValueNodeInterface
{
/** @var ParserInterface $parser */
$parser = static::make(ParserInterface::class);
return $parser->parseValue($source, $options);
}
/**
* @param Source $source
* @param array $options
* @return TypeNodeInterface
*/
public static function parseType(Source $source, array $options = []): TypeNodeInterface
{
/** @var ParserInterface $parser */
$parser = static::make(ParserInterface::class);
return $parser->parseType($source, $options);
}
/**
* @param Schema $schema
* @param DocumentNode $document
* @return array
*/
public static function validate(Schema $schema, DocumentNode $document): array
{
/** @var ValidatorInterface $validator */
$validator = static::make(ValidatorInterface::class);
return $validator->validate($schema, $document);
}
/**
* @param Schema $schema
* @param DocumentNode $document
* @param mixed $rootValue
* @param mixed $contextValue
* @param array $variableValues
* @param string|null $operationName
* @param callable|null $fieldResolver
* @param ErrorHandlerInterface|null $errorHandler
* @return PromiseInterface
*/
public static function execute(
Schema $schema,
DocumentNode $document,
$rootValue = null,
$contextValue = null,
array $variableValues = [],
$operationName = null,
callable $fieldResolver = null,
?ErrorHandlerInterface $errorHandler = null
): PromiseInterface {
/** @var ExecutionInterface $execution */
$execution = static::make(ExecutionInterface::class);
return $execution->execute(
$schema,
$document,
$rootValue,
$contextValue,
$variableValues,
$operationName,
$fieldResolver,
$errorHandler
);
}
/**
* @param Schema $schema
* @param string $source
* @param mixed $rootValue
* @param mixed $contextValue
* @param array $variableValues
* @param null|string $operationName
* @param callable|null $fieldResolver
* @param ErrorHandlerInterface|null $errorHandler
* @return PromiseInterface
* @throws InvariantException
*/
public static function process(
Schema $schema,
string $source,
$rootValue = null,
$contextValue = null,
array $variableValues = [],
?string $operationName = null,
?callable $fieldResolver = null,
?ErrorHandlerInterface $errorHandler = null
): PromiseInterface {
$schemaValidationErrors = validateSchema($schema);
if (!empty($schemaValidationErrors)) {
if (null !== $errorHandler) {
foreach ($schemaValidationErrors as $schemaValidationError) {
$errorHandler->handleError($schemaValidationError);
}
}
return resolve(new ExecutionResult(null, $schemaValidationErrors));
}
try {
$document = parse($source);
} catch (SyntaxErrorException $error) {
if (null !== $errorHandler) {
$errorHandler->handleError($error);
}
return resolve(new ExecutionResult(null, [$error]));
}
$validationErrors = validate($schema, $document);
if (!empty($validationErrors)) {
if (null !== $errorHandler) {
foreach ($validationErrors as $validationError) {
$errorHandler->handleError($validationError);
}
}
return resolve(new ExecutionResult(null, $validationErrors));
}
return executeAsync(
$schema,
$document,
$rootValue,
$contextValue,
$variableValues,
$operationName,
$fieldResolver,
$errorHandler
);
}
/**
* @param NodeInterface $node
* @return string
*/
public static function print(NodeInterface $node): string
{
/** @var NodePrinterInterface $nodePrinter */
$nodePrinter = static::make(NodePrinterInterface::class);
return $nodePrinter->print($node);
}
/**
* @return Container
*/
public function getContainer(): Container
{
return $this->container;
}
/**
* Registers the service provides with the container.
*
* @param Container $container
*/
protected function registerProviders(Container $container): void
{
foreach (self::$providers as $className) {
$container->addServiceProvider($className);
}
}
}
================================================
FILE: src/Language/DirectiveLocationEnum.php
================================================
getConstants());
}
}
================================================
FILE: src/Language/FileSourceBuilder.php
================================================
filePath = $filePath;
}
/**
* @inheritdoc
*
* @throws FileNotFoundException
* @throws InvariantException
*/
public function build(): Source
{
if (!\file_exists($this->filePath) || !\is_readable($this->filePath)) {
throw new FileNotFoundException(sprintf('The file %s cannot be found or is not readable', $this->filePath));
}
return new Source(\file_get_contents($this->filePath));
}
}
================================================
FILE: src/Language/KeywordEnum.php
================================================
getConstants());
}
}
================================================
FILE: src/Language/LanguageException.php
================================================
container->share(NodeBuilderInterface::class, NodeBuilder::class);
$this->container->share(NodePrinterInterface::class, NodePrinter::class);
$this->container->add(ParserInterface::class, Parser::class);
}
}
================================================
FILE: src/Language/Lexer.php
================================================
TokenKindEnum::BANG,
36 => TokenKindEnum::DOLLAR,
38 => TokenKindEnum::AMP,
40 => TokenKindEnum::PAREN_L,
41 => TokenKindEnum::PAREN_R,
58 => TokenKindEnum::COLON,
61 => TokenKindEnum::EQUALS,
64 => TokenKindEnum::AT,
91 => TokenKindEnum::BRACKET_L,
93 => TokenKindEnum::BRACKET_R,
123 => TokenKindEnum::BRACE_L,
124 => TokenKindEnum::PIPE,
125 => TokenKindEnum::BRACE_R,
];
/**
* The source file for this lexer.
*
* @var Source
*/
protected $source;
/**
* The contents of the source file.
*
* @var string
*/
protected $body;
/**
* The total number of characters in the source file.
*
* @var int
*/
protected $bodyLength;
/**
* The options for this lexer.
*
* @var array
*/
protected $options = [];
/**
* The previously focused non-ignored token.
*
* @var Token
*/
protected $lastToken;
/**
* The currently focused non-ignored token.
*
* @var Token
*/
protected $token;
/**
* The current position.
*
* @var int
*/
protected $position;
/**
* The (1-indexed) line containing the current token.
*
* @var int
*/
protected $line;
/**
* The character offset at which the current line begins.
*
* @var int
*/
protected $lineStart;
/**
* A key-value map over characters and their corresponding character codes.
*
* @var array
*/
protected static $charCodeCache = [];
/**
* Lexer constructor.
* @param Source $source
* @param array $options
*/
public function __construct(Source $source, array $options)
{
$startOfFileToken = $this->createStartOfFileToken();
$this->lastToken = $startOfFileToken;
$this->token = $startOfFileToken;
$this->line = 1;
$this->lineStart = 0;
$this->body = $source->getBody();
$this->bodyLength = \mb_strlen($this->body);
$this->source = $source;
$this->options = $options;
}
/**
* @inheritdoc
* @throws SyntaxErrorException
*/
public function advance(): Token
{
$this->lastToken = $this->token;
return $this->token = $this->lookahead();
}
/**
* @inheritdoc
* @throws SyntaxErrorException
*/
public function lookahead(): Token
{
$token = $this->token;
if (TokenKindEnum::EOF !== $token->getKind()) {
do {
$next = $this->readToken($token);
$token->setNext($next);
$token = $next;
} while (TokenKindEnum::COMMENT === $token->getKind());
}
return $token;
}
/**
* @inheritdoc
*/
public function getOption(string $name, $default = null)
{
return $this->options[$name] ?? $default;
}
/**
* @inheritdoc
*/
public function getSource(): Source
{
return $this->source;
}
/**
* @inheritdoc
*/
public function getToken(): Token
{
return $this->token;
}
/**
* @inheritdoc
*/
public function getLastToken(): Token
{
return $this->lastToken;
}
/**
* @inheritdoc
*/
public function createSyntaxErrorException(?string $description = null): SyntaxErrorException
{
return new SyntaxErrorException(
$this->source,
$this->position,
$description ?? $this->unexpectedCharacterMessage($this->readCharCode($this->position))
);
}
/**
* Reads the token after the given token.
*
* @param Token $prev
* @return Token
* @throws SyntaxErrorException
*/
protected function readToken(Token $prev): Token
{
$this->position = $prev->getEnd();
$this->skipWhitespace();
$line = $this->line;
$column = (1 + $this->position) - $this->lineStart;
if ($this->position >= $this->bodyLength) {
return $this->createEndOfFileToken($line, $column, $prev);
}
$code = $this->readCharCode($this->position);
// Punctuation: [!$&:=@|()\[\]{}]{1}
if (33 === $code || 36 === $code || 38 === $code || 58 === $code || 61 === $code || 64 === $code || 124 === $code ||
40 === $code || 41 === $code || 91 === $code || 93 === $code || 123 === $code || 125 === $code) {
return $this->lexPunctuation($code, $line, $column, $prev);
}
// Comment: #[\u0009\u0020-\uFFFF]*
if (35 === $code) {
return $this->lexComment($line, $column, $prev);
}
// Int: -?(0|[1-9][0-9]*)
// Float: -?(0|[1-9][0-9]*)(\.[0-9]+)?((E|e)(+|-)?[0-9]+)?
if (45 === $code || isNumber($code)) {
return $this->lexNumber($code, $line, $column, $prev);
}
// Name: [_A-Za-z][_0-9A-Za-z]*
if (isAlphaNumeric($code)) {
return $this->lexName($line, $column, $prev);
}
// Spread: ...
if ($this->bodyLength >= 3 && $this->isSpread($code)) {
return $this->lexSpread($line, $column, $prev);
}
// String: "([^"\\\u000A\u000D]|(\\(u[0-9a-fA-F]{4}|["\\/bfnrt])))*"
if ($this->isString($code)) {
return $this->lexString($line, $column, $prev);
}
// Block String: """("?"?(\\"""|\\(?!=""")|[^"\\]))*"""
if ($this->bodyLength >= 3 && $this->isTripleQuote($code)) {
return $this->lexBlockString($line, $column, $prev);
}
throw $this->createSyntaxErrorException();
}
/**
* @return Token
*/
protected function createStartOfFileToken(): Token
{
return new Token(TokenKindEnum::SOF);
}
/**
* Creates an End Of File (EOF) token.
*
* @param int $line
* @param int $column
* @param Token $prev
* @return Token
*/
protected function createEndOfFileToken(int $line, int $column, Token $prev): Token
{
return new Token(TokenKindEnum::EOF, $this->bodyLength, $this->bodyLength, $line, $column, $prev);
}
/**
* Reads a punctuation token from the source file.
*
* @param int $code
* @param int $line
* @param int $column
* @param Token $prev
* @return Token
* @throws SyntaxErrorException
*/
protected function lexPunctuation(int $code, int $line, int $column, Token $prev): ?Token
{
if (!isset(self::$codeTokenKindMap[$code])) {
throw $this->createSyntaxErrorException();
}
return new Token(self::$codeTokenKindMap[$code], $this->position, $this->position + 1, $line, $column, $prev);
}
/**
* Reads a name token from the source file.
*
* @param int $line
* @param int $column
* @param Token $prev
* @return Token
*/
protected function lexName(int $line, int $column, Token $prev): Token
{
$start = $this->position;
++$this->position;
while ($this->position !== $this->bodyLength &&
($code = $this->readCharCode($this->position)) !== 0 &&
isAlphaNumeric($code)) {
++$this->position;
}
$value = sliceString($this->body, $start, $this->position);
return new Token(TokenKindEnum::NAME, $start, $this->position, $line, $column, $prev, $value);
}
/**
* Reads a number (int or float) token from the source file.
*
* @param int $code
* @param int $line
* @param int $column
* @param Token $prev
* @return Token
* @throws SyntaxErrorException
*/
protected function lexNumber(int $code, int $line, int $column, Token $prev): Token
{
$start = $this->position;
$isFloat = false;
if (45 === $code) {
// -
$code = $this->readCharCode(++$this->position);
}
if (48 === $code) {
// 0
$code = $this->readCharCode(++$this->position);
if (isNumber($code)) {
throw $this->createSyntaxErrorException(
\sprintf('Invalid number, unexpected digit after 0: %s.', printCharCode($code))
);
}
} else {
$this->skipDigits($code);
$code = $this->readCharCode($this->position);
}
if (46 === $code) {
// .
$isFloat = true;
$code = $this->readCharCode(++$this->position);
$this->skipDigits($code);
$code = $this->readCharCode($this->position);
}
if (69 === $code || 101 === $code) {
// e or E
$isFloat = true;
$code = $this->readCharCode(++$this->position);
if (43 === $code || 45 === $code) {
// + or -
$code = $this->readCharCode(++$this->position);
}
$this->skipDigits($code);
}
return new Token(
$isFloat ? TokenKindEnum::FLOAT : TokenKindEnum::INT,
$start,
$this->position,
$line,
$column,
$prev,
sliceString($this->body, $start, $this->position)
);
}
/**
* Skips digits at the current position.
*
* @param int $code
* @throws SyntaxErrorException
*/
protected function skipDigits(int $code): void
{
if (isNumber($code)) {
do {
$code = $this->readCharCode(++$this->position);
} while (isNumber($code));
return;
}
throw $this->createSyntaxErrorException(
\sprintf('Invalid number, expected digit but got: %s.', printCharCode($code))
);
}
/**
* Reads a comment token from the source file.
*
* @param int $line
* @param int $column
* @param Token $prev
* @return Token
*/
protected function lexComment(int $line, int $column, Token $prev): Token
{
$start = $this->position;
do {
$code = $this->readCharCode(++$this->position);
} while ($code !== 0 && ($code > 0x001f || 0x0009 === $code)); // SourceCharacter but not LineTerminator
return new Token(
TokenKindEnum::COMMENT,
$start,
$this->position,
$line,
$column,
$prev,
sliceString($this->body, $start + 1, $this->position)
);
}
/**
* Reads a spread token from the source.
*
* @param int $line
* @param int $column
* @param Token $prev
* @return Token
*/
protected function lexSpread(int $line, int $column, Token $prev): Token
{
return new Token(TokenKindEnum::SPREAD, $this->position, $this->position + 3, $line, $column, $prev);
}
/**
* Reads a string token from the source.
*
* @param int $line
* @param int $column
* @param Token $prev
* @return Token
* @throws SyntaxErrorException
*/
protected function lexString(int $line, int $column, Token $prev): Token
{
$start = $this->position;
$chunkStart = ++$this->position; // skip the quote
$value = '';
while ($this->position < $this->bodyLength) {
$code = $this->readCharCode($this->position);
if (isLineTerminator($code)) {
break;
}
// Closing Quote (")
if (34 === $code) {
$value .= sliceString($this->body, $chunkStart, $this->position);
return new Token(TokenKindEnum::STRING, $start, $this->position + 1, $line, $column, $prev, $value);
}
if (isSourceCharacter($code)) {
throw $this->createSyntaxErrorException(
\sprintf('Invalid character within String: %s.', printCharCode($code))
);
}
++$this->position;
if (92 === $code) {
// \
$value .= sliceString($this->body, $chunkStart, $this->position - 1);
$code = $this->readCharCode($this->position);
switch ($code) {
case 34: // "
$value .= '"';
break;
case 47: // /
$value .= '/';
break;
case 92: // \
$value .= '\\';
break;
case 98: // b
$value .= '\b';
break;
case 102: // f
$value .= '\f';
break;
case 110: // n
$value .= '\n';
break;
case 114: // r
$value .= '\r';
break;
case 116: // t
$value .= '\t';
break;
case 117: // u
$unicodeString = sliceString($this->body, $this->position + 1, $this->position + 5);
if (!\preg_match('/[0-9A-Fa-f]{4}/', $unicodeString)) {
throw $this->createSyntaxErrorException(
\sprintf('Invalid character escape sequence: \\u%s.', $unicodeString)
);
}
$value .= '\\u' . $unicodeString;
$this->position += 4;
break;
default:
throw $this->createSyntaxErrorException(
\sprintf('Invalid character escape sequence: \\%s.', \chr($code))
);
}
++$this->position;
$chunkStart = $this->position;
}
}
throw $this->createSyntaxErrorException('Unterminated string.');
}
/**
* Reads a block string token from the source file.
*
* @param int $line
* @param int $column
* @param Token $prev
* @return Token
* @throws SyntaxErrorException
*/
protected function lexBlockString(int $line, int $column, Token $prev): Token
{
$start = $this->position;
$this->position = $start + 3; // skip the triple-quote
$chunkStart = $this->position;
$rawValue = '';
while ($this->position < $this->bodyLength) {
$code = $this->readCharCode($this->position);
// Closing Triple-Quote (""")
if ($this->isTripleQuote($code)) {
$rawValue .= sliceString($this->body, $chunkStart, $this->position);
return new Token(
TokenKindEnum::BLOCK_STRING,
$start,
$this->position + 3,
$line,
$column,
$prev,
blockStringValue($rawValue)
);
}
if (isSourceCharacter($code) && !isLineTerminator($code)) {
throw $this->createSyntaxErrorException(
\sprintf('Invalid character within String: %s.', printCharCode($code))
);
}
if ($this->isEscapedTripleQuote($code)) {
$rawValue .= sliceString($this->body, $chunkStart, $this->position) . '"""';
$this->position += 4;
$chunkStart = $this->position;
} else {
++$this->position;
}
}
throw $this->createSyntaxErrorException('Unterminated string.');
}
/**
* Skips whitespace at the current position.
*/
protected function skipWhitespace(): void
{
while ($this->position < $this->bodyLength) {
$code = $this->readCharCode($this->position);
if (9 === $code || 32 === $code || 44 === $code || 0xfeff === $code) {
// tab | space | comma | BOM
++$this->position;
} elseif (10 === $code) {
// new line (\n)
++$this->position;
++$this->line;
$this->lineStart = $this->position;
} elseif (13 === $code) {
// carriage return (\r)
if (10 === $this->readCharCode($this->position + 1)) {
// carriage return and new line (\r\n)
$this->position += 2;
} else {
++$this->position;
}
++$this->line;
$this->lineStart = $this->position;
} else {
break;
}
}
}
/**
* @param int $position
* @return int
*/
protected function readCharCode(int $position): int
{
$char = \mb_substr($this->body, $position, 1, self::ENCODING);
if ('' === $char) {
return 0;
}
if (!isset(self::$charCodeCache[$char])) {
$code = \ord($char);
if ($code >= 128) {
$code = \mb_ord($char, self::ENCODING);
}
self::$charCodeCache[$char] = $code;
}
return self::$charCodeCache[$char];
}
/**
* Report a message that an unexpected character was encountered.
*
* @param int $code
* @return string
*/
protected function unexpectedCharacterMessage(int $code): string
{
if (isSourceCharacter($code) && !isLineTerminator($code)) {
return \sprintf('Cannot contain the invalid character %s.', printCharCode($code));
}
if ($code === 39) {
// '
return 'Unexpected single quote character (\'), did you mean to use a double quote (")?';
}
return \sprintf('Cannot parse the unexpected character %s.', printCharCode($code));
}
/**
* @param int $code
* @return bool
*/
protected function isSpread(int $code): bool
{
return 46 === $code &&
$this->readCharCode($this->position + 1) === 46 &&
$this->readCharCode($this->position + 2) === 46; // ...
}
/**
* @param int $code
* @return bool
*/
protected function isString(int $code): bool
{
return 34 === $code && $this->readCharCode($this->position + 1) !== 34;
}
/**
* @param int $code
* @return bool
*/
protected function isTripleQuote(int $code): bool
{
return 34 === $code &&
34 === $this->readCharCode($this->position + 1) &&
34 === $this->readCharCode($this->position + 2); // """
}
/**
* @param int $code
* @return bool
*/
protected function isEscapedTripleQuote(int $code): bool
{
return $code === 92 &&
34 === $this->readCharCode($this->position + 1) &&
34 === $this->readCharCode($this->position + 2) &&
34 === $this->readCharCode($this->position + 3); // \"""
}
}
================================================
FILE: src/Language/LexerInterface.php
================================================
start = $start;
$this->end = $end;
$this->source = $source;
}
/**
* @return int
*/
public function getStart(): int
{
return $this->start;
}
/**
* @return int
*/
public function getEnd(): int
{
return $this->end;
}
/**
* @return Source|null
*/
public function getSource(): ?Source
{
return $this->source;
}
/**
* @return array
*/
public function toArray(): array
{
return [
'start' => $this->start,
'end' => $this->end,
];
}
/**
* @return string
*/
public function __toString(): string
{
return $this->toJSON();
}
}
================================================
FILE: src/Language/MultiFileSourceBuilder.php
================================================
filePaths = $filePaths;
}
/**
* @inheritdoc
*
* @throws FileNotFoundException
* @throws InvariantException
*/
public function build(): Source
{
$combinedSource = '';
foreach ($this->filePaths as $filePath) {
if (!\file_exists($filePath) || !\is_readable($filePath)) {
throw new FileNotFoundException(sprintf('The file %s cannot be found or is not readable', $filePath));
}
$combinedSource .= \file_get_contents($filePath);
}
return new Source($combinedSource);
}
}
================================================
FILE: src/Language/Node/ASTNodeAwareInterface.php
================================================
astNode;
}
/**
* @return NodeInterface|null
*/
public function getAstNode(): ?NodeInterface
{
return $this->astNode;
}
/**
* @param NodeInterface|null $astNode
* @return $this
*/
protected function setAstNode(?NodeInterface $astNode)
{
$this->astNode = $astNode;
return $this;
}
}
================================================
FILE: src/Language/Node/AbstractNode.php
================================================
kind = $kind;
$this->location = $location;
}
/**
* @return string
*/
public function getKind(): string
{
return $this->kind;
}
/**
* @return bool
*/
public function hasLocation(): bool
{
return null !== $this->location;
}
/**
* @return Location|null
*/
public function getLocation(): ?Location
{
return $this->location;
}
/**
* @return array|null
*/
public function getLocationAST(): ?array
{
return null !== $this->location
? $this->location->toArray()
: null;
}
/**
* @return array
*/
public function toArray(): array
{
return $this->toAST();
}
/**
* @return string
*/
public function __toString(): string
{
return $this->toJSON();
}
/**
* @inheritdoc
*/
public function acceptVisitor(VisitorInfo $visitorInfo): ?NodeInterface
{
$this->visitorInfo = $visitorInfo;
$visitor = $this->visitorInfo->getVisitor();
$VisitorResult = $visitor->enterNode(clone $this);
$newNode = $VisitorResult->getValue();
// Handle early exit while entering
if ($VisitorResult->getAction() === VisitorResult::ACTION_BREAK) {
/** @noinspection PhpUnhandledExceptionInspection */
throw new VisitorBreak();
}
// If the result was null, it means that we should not traverse this branch.
if (null === $newNode) {
return null;
}
// If the node was edited, we want to return early to avoid visiting its sub-tree completely.
if ($newNode->determineIsEdited($this)) {
return $newNode;
}
foreach (self::$kindToNodesToVisitMap[$this->kind] as $property) {
$nodeOrNodes = $this->{$property};
if (empty($nodeOrNodes)) {
continue;
}
$newNodeOrNodes = $this->visitNodeOrNodes($nodeOrNodes, $property, $newNode);
if (empty($newNodeOrNodes)) {
continue;
}
$setter = 'set' . \ucfirst($property);
if (\method_exists($newNode, $setter)) {
$newNode->{$setter}($newNodeOrNodes);
}
}
$VisitorResult = $visitor->leaveNode($newNode);
// Handle early exit while leaving
if ($VisitorResult->getAction() === VisitorResult::ACTION_BREAK) {
/** @noinspection PhpUnhandledExceptionInspection */
throw new VisitorBreak();
}
return $VisitorResult->getValue();
}
/**
* @return VisitorInfo|null
*/
public function getVisitorInfo(): ?VisitorInfo
{
return $this->visitorInfo;
}
/**
* @inheritdoc
*/
public function determineIsEdited(NodeInterface $node): bool
{
return $this->isEdited = $this->isEdited() || !NodeComparator::compare($this, $node);
}
/**
* @inheritdoc
*/
public function getAncestor(int $depth = 1): ?NodeInterface
{
return null !== $this->visitorInfo ? $this->visitorInfo->getAncestor($depth) : null;
}
/**
* @return bool
*/
public function isEdited(): bool
{
return $this->isEdited;
}
/**
* @param NodeInterface|NodeInterface[] $nodeOrNodes
* @param mixed $key
* @param NodeInterface $parent
* @return NodeInterface|NodeInterface[]|null
*/
protected function visitNodeOrNodes($nodeOrNodes, $key, NodeInterface $parent)
{
$this->visitorInfo->addAncestor($parent);
$newNodeOrNodes = \is_array($nodeOrNodes)
? $this->visitNodes($nodeOrNodes, $key)
: $this->visitNode($nodeOrNodes, $key, $parent);
$this->visitorInfo->removeAncestor();
return $newNodeOrNodes;
}
/**
* @param NodeInterface[] $nodes
* @param string|int $key
* @return NodeInterface[]
*/
protected function visitNodes(array $nodes, $key): array
{
$this->visitorInfo->addOneToPath($key);
$index = 0;
$newNodes = [];
foreach ($nodes as $node) {
$newNode = $this->visitNode($node, $index, null);
if (null !== $newNode) {
$newNodes[$index] = $newNode;
$index++;
}
}
$this->visitorInfo->removeOneFromPath();
return $newNodes;
}
/**
* @param NodeInterface $node
* @param string|int $key
* @param NodeInterface|null $parent
* @return NodeInterface|null
*/
protected function visitNode(NodeInterface $node, $key, ?NodeInterface $parent): ?NodeInterface
{
$this->visitorInfo->addOneToPath($key);
$info = new VisitorInfo(
$this->visitorInfo->getVisitor(),
$key,
$parent,
$this->visitorInfo->getPath(),
$this->visitorInfo->getAncestors()
);
$newNode = $node->acceptVisitor($info);
// If the node was edited, we need to revisit it to produce the expected result.
if (null !== $newNode && $newNode->isEdited()) {
$newNode = $newNode->acceptVisitor($info);
}
$this->visitorInfo->removeOneFromPath();
return $newNode;
}
/**
* @param NodeInterface $other
* @return bool
*/
protected function compareNode(NodeInterface $other)
{
return $this->toJSON() === $other->toJSON();
}
/**
* @return NodeBuilderInterface
*/
protected function getNodeBuilder(): NodeBuilderInterface
{
if (null === self::$nodeBuilder) {
self::$nodeBuilder = GraphQL::make(NodeBuilderInterface::class);
}
return self::$nodeBuilder;
}
/**
* @var array
*/
protected static $kindToNodesToVisitMap = [
'Name' => [],
'Document' => ['definitions'],
'OperationDefinition' => [
'name',
'variableDefinitions',
'directives',
'selectionSet',
],
'VariableDefinition' => ['variable', 'type', 'defaultValue'],
'Variable' => ['name'],
'SelectionSet' => ['selections'],
'Field' => ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
'Argument' => ['name', 'value'],
'FragmentSpread' => ['name', 'directives'],
'InlineFragment' => ['typeCondition', 'directives', 'selectionSet'],
'FragmentDefinition' => [
'name',
'variableDefinitions',
'typeCondition',
'directives',
'selectionSet',
],
'IntValue' => [],
'FloatValue' => [],
'StringValue' => [],
'BooleanValue' => [],
'NullValue' => [],
'EnumValue' => [],
'ListValue' => ['values'],
'ObjectValue' => ['fields'],
'ObjectField' => ['name', 'value'],
'Directive' => ['name', 'arguments'],
'NamedType' => ['name'],
'ListType' => ['type'],
'NonNullType' => ['type'],
'SchemaDefinition' => ['directives', 'operationTypes'],
'OperationTypeDefinition' => ['type'],
'ScalarTypeDefinition' => ['description', 'name', 'directives'],
'ObjectTypeDefinition' => [
'description',
'name',
'interfaces',
'directives',
'fields',
],
'FieldDefinition' => ['description', 'name', 'arguments', 'type', 'directives'],
'InputValueDefinition' => [
'description',
'name',
'type',
'defaultValue',
'directives',
],
'InterfaceTypeDefinition' => ['description', 'name', 'directives', 'fields'],
'UnionTypeDefinition' => ['description', 'name', 'directives', 'types'],
'EnumTypeDefinition' => ['description', 'name', 'directives', 'values'],
'EnumValueDefinition' => ['description', 'name', 'directives'],
'InputObjectTypeDefinition' => ['description', 'name', 'directives', 'fields'],
'DirectiveDefinition' => ['description', 'name', 'arguments', 'locations'],
'SchemaExtension' => ['directives', 'operationTypes'],
'ScalarTypeExtension' => ['name', 'directives'],
'ObjectTypeExtension' => ['name', 'interfaces', 'directives', 'fields'],
'InterfaceTypeExtension' => ['name', 'directives', 'fields'],
'UnionTypeExtension' => ['name', 'directives', 'types'],
'EnumTypeExtension' => ['name', 'directives', 'values'],
'InputObjectTypeExtension' => ['name', 'directives', 'fields'],
];
}
================================================
FILE: src/Language/Node/AliasTrait.php
================================================
alias;
}
/**
* @return null|string
*/
public function getAliasValue(): ?string
{
return null !== $this->alias ? $this->alias->getValue() : null;
}
/**
* @return null|string
*/
public function getAliasOrNameValue(): ?string
{
return $this->getAliasValue() ?? $this->getNameValue();
}
/**
* @return array|null
*/
public function getAliasAST(): ?array
{
return null !== $this->alias ? $this->alias->toAST() : null;
}
/**
* @param NameNode|null $alias
* @return $this
*/
public function setAlias(?NameNode $alias)
{
$this->alias = $alias;
return $this;
}
}
================================================
FILE: src/Language/Node/ArgumentNode.php
================================================
name = $name;
$this->value = $value;
}
/**
* @return array
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'name' => $this->getNameAST(),
'value' => $this->getValueAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/ArgumentsAwareInterface.php
================================================
arguments);
}
/**
* @return ArgumentNode[]
*/
public function getArguments(): array
{
return $this->arguments ?? [];
}
/**
* @return array
*/
public function getArgumentsAST(): array
{
return \array_map(function (ArgumentNode $node) {
return $node->toAST();
}, $this->getArguments());
}
/**
* @param ArgumentNode[] $arguments
* @return $this
*/
public function setArguments(array $arguments)
{
$this->arguments = $arguments;
return $this;
}
}
================================================
FILE: src/Language/Node/BooleanValueNode.php
================================================
value = $value;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'value' => $this->value,
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/DefaultValueTrait.php
================================================
defaultValue;
}
/**
* @return ValueNodeInterface|null
*/
public function getDefaultValue(): ?ValueNodeInterface
{
return $this->defaultValue;
}
/**
* @return array
*/
public function getDefaultValueAST(): ?array
{
return null !== $this->defaultValue ? $this->defaultValue->toAST() : null;
}
}
================================================
FILE: src/Language/Node/DefinitionNodeInterface.php
================================================
description;
}
/**
* @return null|string
*/
public function getDescriptionValue(): ?string
{
return null !== $this->description ? $this->description->getValue() : null;
}
/**
* @return array|null
*/
public function getDescriptionAST(): ?array
{
return null !== $this->description ? $this->description->toAST() : null;
}
/**
* @param StringValueNode|null $description
* @return $this
*/
public function setDescription(?StringValueNode $description)
{
$this->description = $description;
return $this;
}
}
================================================
FILE: src/Language/Node/DirectiveDefinitionNode.php
================================================
description = $description;
$this->name = $name;
$this->arguments = $arguments;
$this->locations = $locations;
}
/**
* @return NameNode[]
*/
public function getLocations(): array
{
return $this->locations;
}
/**
* @return array
*/
public function getLocationsAST(): array
{
return \array_map(function (NameNode $node) {
return $node->toAST();
}, $this->locations);
}
/**
* @param NameNode[] $locations
* @return $this
*/
public function setLocations(array $locations)
{
$this->locations = $locations;
return $this;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'description' => $this->getDescriptionAST(),
'name' => $this->getNameAST(),
'arguments' => $this->getArgumentsAST(),
'locations' => $this->getLocationsAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/DirectiveNode.php
================================================
name = $name;
$this->arguments = $arguments;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'name' => $this->getNameAST(),
'arguments' => $this->getArgumentsAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/DirectivesAwareInterface.php
================================================
directives);
}
/**
* @return DirectiveNode[]
*/
public function getDirectives(): array
{
return $this->directives;
}
/**
* @return array
*/
public function getDirectivesAST(): array
{
return \array_map(function (DirectiveNode $directive) {
return $directive->toAST();
}, $this->directives);
}
/**
* @param array|DirectiveNode[] $directives
* @return $this
*/
public function setDirectives(array $directives)
{
$this->directives = $directives;
return $this;
}
}
================================================
FILE: src/Language/Node/DocumentNode.php
================================================
definitions = $definitions;
}
/**
* @return DefinitionNodeInterface[]|TypeSystemExtensionNodeInterface[]
*/
public function getDefinitions(): array
{
return $this->definitions;
}
/**
* @return array
*/
public function getDefinitionsAST(): array
{
return \array_map(function (NodeInterface $node) {
return $node->toAST();
}, $this->definitions);
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'definitions' => $this->getDefinitionsAST(),
'loc' => $this->getLocationAST(),
];
}
/**
* @param DefinitionNodeInterface[] $definitions
* @return DocumentNode
*/
protected function setDefinitions(array $definitions): DocumentNode
{
$this->definitions = $definitions;
return $this;
}
}
================================================
FILE: src/Language/Node/EnumTypeDefinitionNode.php
================================================
description = $description;
$this->name = $name;
$this->directives = $directives;
$this->values = $values;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'description' => $this->getDescriptionAST(),
'name' => $this->getNameAST(),
'directives' => $this->getDirectivesAST(),
'values' => $this->getValuesAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/EnumTypeExtensionNode.php
================================================
name = $name;
$this->directives = $directives;
$this->values = $values;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'name' => $this->getNameAST(),
'directives' => $this->getDirectivesAST(),
'values' => $this->getValuesAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/EnumValueDefinitionNode.php
================================================
description = $description;
$this->name = $name;
$this->directives = $directives;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'description' => $this->getDescriptionAST(),
'name' => $this->getNameAST(),
'directives' => $this->getDirectivesAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/EnumValueNode.php
================================================
value = $value;
}
/**
* @inheritdoc
*/
public function __toString(): string
{
return (string)$this->value;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'value' => $this->value,
];
}
}
================================================
FILE: src/Language/Node/EnumValuesTrait.php
================================================
values);
}
/**
* @return EnumValueDefinitionNode[]
*/
public function getValues(): array
{
return $this->values;
}
/**
* @return array
*/
public function getValuesAST(): array
{
return \array_map(function (EnumValueDefinitionNode $node) {
return $node->toAST();
}, $this->values);
}
/**
* @param array|EnumValueDefinitionNode[] $values
* @return $this
*/
public function setValues(array $values)
{
$this->values = $values;
return $this;
}
}
================================================
FILE: src/Language/Node/ExecutableDefinitionNodeInterface.php
================================================
description = $description;
$this->name = $name;
$this->arguments = $arguments;
$this->type = $type;
$this->directives = $directives;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'description' => $this->description,
'name' => $this->getNameAST(),
'arguments' => $this->getArgumentsAST(),
'type' => $this->getTypeAST(),
'directives' => $this->getDirectivesAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/FieldNode.php
================================================
alias = $alias;
$this->name = $name;
$this->arguments = $arguments;
$this->directives = $directives;
$this->selectionSet = $selectionSet;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'loc' => $this->getLocationAST(),
'alias' => $this->getAliasAST(),
'name' => $this->getNameAST(),
'arguments' => $this->getArgumentsAST(),
'directives' => $this->getDirectivesAST(),
'selectionSet' => $this->getSelectionSetAST(),
];
}
}
================================================
FILE: src/Language/Node/FieldsTrait.php
================================================
fields);
}
/**
* @return FieldDefinitionNode[]
*/
public function getFields(): array
{
return $this->fields;
}
/**
* @return array
*/
public function getFieldsAST(): array
{
return \array_map(function (FieldDefinitionNode $node) {
return $node->toAST();
}, $this->fields);
}
/**
* @param array|FieldDefinitionNode[] $fields
* @return $this
*/
public function setFields(array $fields)
{
$this->fields = $fields;
return $this;
}
}
================================================
FILE: src/Language/Node/FloatValueNode.php
================================================
value = $value;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'loc' => $this->getLocationAST(),
'value' => $this->value,
];
}
}
================================================
FILE: src/Language/Node/FragmentDefinitionNode.php
================================================
name = $name;
$this->variableDefinitions = $variableDefinitions;
$this->typeCondition = $typeCondition;
$this->directives = $directives;
$this->selectionSet = $selectionSet;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'name' => $this->getNameAST(),
'variableDefinitions' => $this->getVariableDefinitionsAST(),
'typeCondition' => $this->getTypeConditionAST(),
'directives' => $this->getDirectivesAST(),
'selectionSet' => $this->getSelectionSetAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/FragmentNodeInterface.php
================================================
name = $name;
$this->directives = $directives;
$this->selectionSet = $selectionSet;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'name' => $this->getNameAST(),
'directives' => $this->getDirectivesAST(),
'selectionSet' => $this->getSelectionSetAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/InlineFragmentNode.php
================================================
typeCondition = $typeCondition;
$this->directives = $directives;
$this->selectionSet = $selectionSet;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'typeCondition' => $this->getTypeConditionAST(),
'directives' => $this->getDirectivesAST(),
'selectionSet' => $this->getSelectionSetAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/InputArgumentsTrait.php
================================================
arguments);
}
/**
* @return InputValueDefinitionNode[]
*/
public function getArguments(): array
{
return $this->arguments ?? [];
}
/**
* @return array
*/
public function getArgumentsAST(): array
{
return \array_map(function (InputValueDefinitionNode $node) {
return $node->toAST();
}, $this->arguments);
}
/**
* @param array|InputValueDefinitionNode[] $arguments
* @return $this
*/
public function setArguments(array $arguments)
{
$this->arguments = $arguments;
return $this;
}
}
================================================
FILE: src/Language/Node/InputFieldsTrait.php
================================================
fields);
}
/**
* @return InputValueDefinitionNode[]
*/
public function getFields(): array
{
return $this->fields;
}
/**
* @return array
*/
public function getFieldsAST(): array
{
return \array_map(function (InputValueDefinitionNode $node) {
return $node->toAST();
}, $this->fields);
}
/**
* @param array|InputValueDefinitionNode[] $fields
* @return $this
*/
public function setFields(array $fields)
{
$this->fields = $fields;
return $this;
}
}
================================================
FILE: src/Language/Node/InputObjectTypeDefinitionNode.php
================================================
description = $description;
$this->name = $name;
$this->directives = $directives;
$this->fields = $fields;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'description' => $this->getDescriptionAST(),
'name' => $this->getNameAST(),
'directives' => $this->getDirectivesAST(),
'fields' => $this->getFieldsAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/InputObjectTypeExtensionNode.php
================================================
name = $name;
$this->directives = $directives;
$this->fields = $fields;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'name' => $this->getNameAST(),
'directives' => $this->getDirectivesAST(),
'fields' => $this->getFieldsAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/InputValueDefinitionNode.php
================================================
description = $description;
$this->name = $name;
$this->type = $type;
$this->defaultValue = $defaultValue;
$this->directives = $directives;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'description' => $this->getDescriptionAST(),
'name' => $this->getNameAST(),
'type' => $this->getTypeAST(),
'defaultValue' => $this->getDefaultValueAST(),
'directives' => $this->getDirectivesAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/IntValueNode.php
================================================
value = $value;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'loc' => $this->getLocationAST(),
'value' => $this->value,
];
}
}
================================================
FILE: src/Language/Node/InterfaceTypeDefinitionNode.php
================================================
description = $description;
$this->name = $name;
$this->directives = $directives;
$this->fields = $fields;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'description' => $this->getDescriptionAST(),
'name' => $this->getNameAST(),
'directives' => $this->getDirectivesAST(),
'fields' => $this->getFieldsAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/InterfaceTypeExtensionNode.php
================================================
name = $name;
$this->directives = $directives;
$this->fields = $fields;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'name' => $this->getNameAST(),
'directives' => $this->getDirectivesAST(),
'fields' => $this->getFieldsAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/InterfacesTrait.php
================================================
interfaces);
}
/**
* @return NamedTypeNode[]
*/
public function getInterfaces(): array
{
return $this->interfaces;
}
/**
* @return array
*/
public function getInterfacesAST(): array
{
return \array_map(function (NamedTypeNode $node) {
return $node->toAST();
}, $this->interfaces);
}
/**
* @param array|NamedTypeNode[] $interfaces
* @return $this
*/
public function setInterfaces(array $interfaces)
{
$this->interfaces = $interfaces;
return $this;
}
}
================================================
FILE: src/Language/Node/ListTypeNode.php
================================================
type = $type;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'type' => $this->getTypeAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/ListValueNode.php
================================================
values = $values;
}
/**
* @return array|ValueNodeInterface[]
*/
public function getValues(): array
{
return $this->values;
}
/**
* @return array
*/
public function getValuesAST(): array
{
return \array_map(function (ValueNodeInterface $node) {
return $node->toAST();
}, $this->values);
}
/**
* @param array|ValueNodeInterface[] $values
* @return $this
*/
public function setValues(array $values)
{
$this->values = $values;
return $this;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'loc' => $this->getLocationAST(),
'values' => $this->getValuesAST(),
];
}
/**
* @inheritdoc
*/
public function __toString(): string
{
return \json_encode(\array_map(function (ValueAwareInterface $node) {
return $node->getValue();
}, $this->getValues()));
}
}
================================================
FILE: src/Language/Node/NameAwareInterface.php
================================================
value = $value;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'value' => $this->value,
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/NameTrait.php
================================================
name;
}
/**
* @return string|null
*/
public function getNameValue(): ?string
{
return null !== $this->name ? $this->name->getValue() : null;
}
/**
* @return array|null
*/
public function getNameAST(): ?array
{
return null !== $this->name ? $this->name->toAST() : null;
}
/**
* @param NameNode|null $name
* @return $this
*/
public function setName(?NameNode $name)
{
$this->name = $name;
return $this;
}
/**
* @inheritdoc
*/
public function __toString(): string
{
return $this->getNameValue() ?? '';
}
}
================================================
FILE: src/Language/Node/NamedTypeNode.php
================================================
name = $name;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'name' => $this->getNameAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/NamedTypeNodeInterface.php
================================================
getConstants());
}
}
================================================
FILE: src/Language/Node/NonNullTypeNode.php
================================================
type = $type;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'type' => $this->getTypeAST(),
'loc' => $this->getLocationAST(),
];
}
/**
* @inheritdoc
*/
public function __toString(): string
{
return (string)$this->type . '!';
}
}
================================================
FILE: src/Language/Node/NullValueNode.php
================================================
$this->kind,
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/ObjectFieldNode.php
================================================
name = $name;
$this->value = $value;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'name' => $this->getNameAST(),
'value' => $this->getValueAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/ObjectTypeDefinitionNode.php
================================================
description = $description;
$this->name = $name;
$this->interfaces = $interfaces;
$this->directives = $directives;
$this->fields = $fields;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'description' => $this->getDescriptionAST(),
'name' => $this->getNameAST(),
'interfaces' => $this->getInterfacesAST(),
'directives' => $this->getDirectivesAST(),
'fields' => $this->getFieldsAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/ObjectTypeExtensionNode.php
================================================
name = $name;
$this->interfaces = $interfaces;
$this->directives = $directives;
$this->fields = $fields;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'name' => $this->getNameAST(),
'interfaces' => $this->getInterfacesAST(),
'directives' => $this->getDirectivesAST(),
'fields' => $this->getFieldsAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/ObjectValueNode.php
================================================
fields = $fields;
}
/**
* @return ObjectFieldNode[]
*/
public function getFields(): array
{
return $this->fields;
}
/**
* @return array
*/
public function getFieldsAST(): array
{
return \array_map(function (ObjectFieldNode $node) {
return $node->toAST();
}, $this->fields);
}
/**
* @param array $fields
* @return $this
*/
public function setFields(array $fields)
{
$this->fields = $fields;
return $this;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'fields' => $this->getFieldsAST(),
];
}
}
================================================
FILE: src/Language/Node/OperationDefinitionNode.php
================================================
operation = $operation;
$this->name = $name;
$this->variableDefinitions = $variableDefinitions;
$this->directives = $directives;
$this->selectionSet = $selectionSet;
}
/**
* @return string
*/
public function getOperation(): string
{
return $this->operation;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'loc' => $this->getLocationAST(),
'operation' => $this->operation,
'name' => $this->getNameAST(),
'variableDefinitions' => $this->getVariableDefinitionsAST(),
'directives' => $this->getDirectivesAST(),
'selectionSet' => $this->getSelectionSetAST(),
];
}
}
================================================
FILE: src/Language/Node/OperationTypeDefinitionNode.php
================================================
operation = $operation;
$this->type = $type;
}
/**
* @return string
*/
public function getOperation(): string
{
return $this->operation;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'operation' => $this->operation,
'type' => $this->getTypeAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/ScalarTypeDefinitionNode.php
================================================
directives = $directives;
$this->description = $description;
$this->name = $name;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'description' => $this->getDescriptionAST(),
'name' => $this->getNameAST(),
'directives' => $this->getDirectivesAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/ScalarTypeExtensionNode.php
================================================
name = $name;
$this->directives = $directives;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'name' => $this->getNameAST(),
'directives' => $this->getDirectivesAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/SchemaDefinitionNode.php
================================================
directives = $directives;
$this->operationTypes = $operationTypes;
}
/**
* @return OperationTypeDefinitionNode[]
*/
public function getOperationTypes(): array
{
return $this->operationTypes;
}
/**
* @return array
*/
public function getOperationTypesAST(): array
{
return \array_map(function (OperationTypeDefinitionNode $node) {
return $node->toAST();
}, $this->operationTypes);
}
/**
* @param OperationTypeDefinitionNode[] $operationTypes
* @return $this
*/
public function setOperationTypes(array $operationTypes)
{
$this->operationTypes = $operationTypes;
return $this;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'directives' => $this->getDirectivesAST(),
'operationTypes' => $this->getOperationTypesAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/SchemaExtensionNode.php
================================================
directives = $directives;
$this->operationTypes = $operationTypes;
}
/**
* @return OperationTypeDefinitionNode[]
*/
public function getOperationTypes(): array
{
return $this->operationTypes;
}
/**
* @return array
*/
public function getOperationTypesAST(): array
{
return \array_map(function (OperationTypeDefinitionNode $node) {
return $node->toAST();
}, $this->operationTypes);
}
/**
* @param OperationTypeDefinitionNode[] $operationTypes
* @return $this
*/
public function setOperationTypes(array $operationTypes)
{
$this->operationTypes = $operationTypes;
return $this;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'directives' => $this->getDirectivesAST(),
'operationTypes' => $this->getOperationTypesAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/SelectionNodeInterface.php
================================================
selections = $selections;
}
/**
* @return SelectionNodeInterface[]
*/
public function getSelections(): array
{
return $this->selections;
}
/**
* @return array
*/
public function getSelectionsAST(): array
{
return \array_map(function (SelectionNodeInterface $node) {
return $node->toAST();
}, $this->selections);
}
/**
* @param SelectionNodeInterface[] $selections
* @return SelectionSetNode
*/
public function setSelections(array $selections): SelectionSetNode
{
$this->selections = $selections;
return $this;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'loc' => $this->getLocationAST(),
'selections' => $this->getSelectionsAST(),
];
}
}
================================================
FILE: src/Language/Node/SelectionSetTrait.php
================================================
selectionSet;
}
/**
* @return SelectionSetNode|null
*/
public function getSelectionSet(): ?SelectionSetNode
{
return $this->selectionSet;
}
/**
* @return array|null
*/
public function getSelectionSetAST(): ?array
{
return null !== $this->selectionSet ? $this->selectionSet->toArray() : null;
}
/**
* @param SelectionSetNode|null $selectionSet
* @return $this
*/
public function setSelectionSet(?SelectionSetNode $selectionSet)
{
$this->selectionSet = $selectionSet;
return $this;
}
}
================================================
FILE: src/Language/Node/StringValueNode.php
================================================
value = $value;
$this->block = $block;
}
/**
* @return bool
*/
public function isBlock(): bool
{
return $this->block;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'loc' => $this->getLocationAST(),
'block' => $this->block,
'value' => $this->value,
];
}
}
================================================
FILE: src/Language/Node/TypeConditionTrait.php
================================================
typeCondition;
}
/**
* @return array|null
*/
public function getTypeConditionAST(): ?array
{
return null !== $this->typeCondition ? $this->typeCondition->toAST() : null;
}
/**
* @param NamedTypeNode|null $typeCondition
* @return $this
*/
public function setTypeCondition(?NamedTypeNode $typeCondition)
{
$this->typeCondition = $typeCondition;
return $this;
}
}
================================================
FILE: src/Language/Node/TypeNodeInterface.php
================================================
type;
}
/**
* @return array
*/
public function getTypeAST(): array
{
return $this->type->toAST();
}
/**
* @param TypeNodeInterface $type
* @return $this
*/
public function setType($type)
{
$this->type = $type;
return $this;
}
}
================================================
FILE: src/Language/Node/TypesTrait.php
================================================
types);
}
/**
* @return NamedTypeNode[]
*/
public function getTypes(): array
{
return $this->types;
}
/**
* @return array
*/
public function getTypesAST(): array
{
return \array_map(function (NamedTypeNode $node) {
return $node->toAST();
}, $this->types);
}
/**
* @param array|NamedTypeNode[] $types
* @return $this
*/
public function setTypes(array $types)
{
$this->types = $types;
return $this;
}
}
================================================
FILE: src/Language/Node/UnionTypeDefinitionNode.php
================================================
description = $description;
$this->name = $name;
$this->directives = $directives;
$this->types = $types;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'description' => $this->getDescriptionAST(),
'name' => $this->getNameAST(),
'directives' => $this->getDirectivesAST(),
'types' => $this->getTypesAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/UnionTypeExtensionNode.php
================================================
name = $name;
$this->directives = $directives;
$this->types = $types;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'name' => $this->getNameAST(),
'directives' => $this->getDirectivesAST(),
'types' => $this->getTypesAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/ValueAwareInterface.php
================================================
value;
}
/**
* @return array|null
*/
public function getValueAST(): ?array
{
return null !== $this->value ? $this->value->toAST() : null;
}
/**
* @param ValueNodeInterface|null $value
* @return $this
*/
public function setValue($value)
{
$this->value = $value;
return $this;
}
}
================================================
FILE: src/Language/Node/ValueNodeInterface.php
================================================
value;
}
/**
* @param mixed|null $value
* @return $this
*/
public function setValue($value)
{
$this->value = $value;
return $this;
}
}
================================================
FILE: src/Language/Node/VariableDefinitionNode.php
================================================
variable = $variable;
$this->type = $type;
$this->defaultValue = $defaultValue;
}
/**
* @return VariableNode
*/
public function getVariable(): VariableNode
{
return $this->variable;
}
/**
* @return array
*/
public function getVariableAST(): array
{
return $this->variable->toAST();
}
/**
* @inheritdoc
*/
public function __toString(): string
{
return (string)$this->type;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'variable' => $this->getVariableAST(),
'type' => $this->getTypeAST(),
'loc' => $this->getLocationAST(),
];
}
}
================================================
FILE: src/Language/Node/VariableDefinitionsAwareInterface.php
================================================
variableDefinitions;
}
/**
* @return array
*/
public function getVariableDefinitionsAST(): array
{
return \array_map(function (VariableDefinitionNode $node) {
return $node->toAST();
}, $this->variableDefinitions);
}
/**
* @param VariableDefinitionNode[] $variableDefinitions
* @return $this
*/
protected function setVariableDefinitions(array $variableDefinitions)
{
$this->variableDefinitions = $variableDefinitions;
return $this;
}
}
================================================
FILE: src/Language/Node/VariableNode.php
================================================
name = $name;
}
/**
* @inheritdoc
*/
public function toAST(): array
{
return [
'kind' => $this->kind,
'name' => $this->getNameAST(),
];
}
}
================================================
FILE: src/Language/NodeBuilder.php
================================================
$kind] = $ast;
switch ($kind) {
case NodeKindEnum::ARGUMENT:
return $this->buildArgument($ast);
case NodeKindEnum::BOOLEAN:
return $this->buildBoolean($ast);
case NodeKindEnum::DIRECTIVE_DEFINITION:
return $this->buildDirectiveDefinition($ast);
case NodeKindEnum::DIRECTIVE:
return $this->buildDirective($ast);
case NodeKindEnum::DOCUMENT:
return $this->buildDocument($ast);
case NodeKindEnum::ENUM:
return $this->buildEnum($ast);
case NodeKindEnum::ENUM_TYPE_DEFINITION:
return $this->buildEnumTypeDefinition($ast);
case NodeKindEnum::ENUM_TYPE_EXTENSION:
return $this->buildEnumTypeExtension($ast);
case NodeKindEnum::ENUM_VALUE_DEFINITION:
return $this->buildEnumValueDefinition($ast);
case NodeKindEnum::FIELD:
return $this->buildField($ast);
case NodeKindEnum::FIELD_DEFINITION:
return $this->buildFieldDefinition($ast);
case NodeKindEnum::FLOAT:
return $this->buildFloat($ast);
case NodeKindEnum::FRAGMENT_DEFINITION:
return $this->buildFragmentDefinition($ast);
case NodeKindEnum::FRAGMENT_SPREAD:
return $this->buildFragmentSpread($ast);
case NodeKindEnum::INLINE_FRAGMENT:
return $this->buildInlineFragment($ast);
case NodeKindEnum::INPUT_OBJECT_TYPE_DEFINITION:
return $this->buildInputObjectTypeDefinition($ast);
case NodeKindEnum::INPUT_OBJECT_TYPE_EXTENSION:
return $this->buildInputObjectTypeExtension($ast);
case NodeKindEnum::INPUT_VALUE_DEFINITION:
return $this->buildInputValueDefinition($ast);
case NodeKindEnum::INTERFACE_TYPE_DEFINITION:
return $this->buildInterfaceTypeDefinition($ast);
case NodeKindEnum::INTERFACE_TYPE_EXTENSION:
return $this->buildInterfaceTypeExtension($ast);
case NodeKindEnum::INT:
return $this->buildInt($ast);
case NodeKindEnum::LIST_TYPE:
return $this->buildListType($ast);
case NodeKindEnum::LIST:
return $this->buildList($ast);
case NodeKindEnum::NAMED_TYPE:
return $this->buildNamedType($ast);
case NodeKindEnum::NAME:
return $this->buildName($ast);
case NodeKindEnum::NON_NULL_TYPE:
return $this->buildNonNullType($ast);
case NodeKindEnum::NULL:
return $this->buildNull($ast);
case NodeKindEnum::OBJECT_FIELD:
return $this->buildObjectField($ast);
case NodeKindEnum::OBJECT_TYPE_DEFINITION:
return $this->buildObjectTypeDefinition($ast);
case NodeKindEnum::OBJECT_TYPE_EXTENSION:
return $this->buildObjectTypeExtension($ast);
case NodeKindEnum::OBJECT:
return $this->buildObject($ast);
case NodeKindEnum::OPERATION_DEFINITION:
return $this->buildOperationDefinition($ast);
case NodeKindEnum::OPERATION_TYPE_DEFINITION:
return $this->buildOperationTypeDefinition($ast);
case NodeKindEnum::SCALAR_TYPE_DEFINITION:
return $this->buildScalarTypeDefinition($ast);
case NodeKindEnum::SCALAR_TYPE_EXTENSION:
return $this->buildScalarTypeExtension($ast);
case NodeKindEnum::SCHEMA_DEFINITION:
return $this->buildSchemaDefinition($ast);
case NodeKindEnum::SCHEMA_EXTENSION:
return $this->buildSchemaExtension($ast);
case NodeKindEnum::SELECTION_SET:
return $this->buildSelectionSet($ast);
case NodeKindEnum::STRING:
return $this->buildString($ast);
case NodeKindEnum::UNION_TYPE_DEFINITION:
return $this->buildUnionTypeDefinition($ast);
case NodeKindEnum::UNION_TYPE_EXTENSION:
return $this->buildUnionTypeExtension($ast);
case NodeKindEnum::VARIABLE_DEFINITION:
return $this->buildVariableDefinition($ast);
case NodeKindEnum::VARIABLE:
return $this->buildVariable($ast);
}
/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
throw new LanguageException(\sprintf('Node of kind "%s" not supported.', $kind));
}
/**
* @param array $ast
* @return ArgumentNode
* @throws LanguageException
*/
protected function buildArgument(array $ast): ArgumentNode
{
return new ArgumentNode(
$this->buildNode($ast, 'name'),
$this->buildNode($ast, 'value'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return BooleanValueNode
*/
protected function buildBoolean(array $ast): BooleanValueNode
{
return new BooleanValueNode(
$this->getValue($ast, 'value'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return DirectiveDefinitionNode
* @throws LanguageException
*/
protected function buildDirectiveDefinition(array $ast): DirectiveDefinitionNode
{
return new DirectiveDefinitionNode(
$this->buildNode($ast, 'description'),
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'arguments'),
$this->buildNodes($ast, 'locations'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return DirectiveNode
* @throws LanguageException
*/
protected function buildDirective(array $ast): DirectiveNode
{
return new DirectiveNode(
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'arguments'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return DocumentNode
* @throws LanguageException
*/
protected function buildDocument(array $ast): DocumentNode
{
return new DocumentNode(
$this->buildNodes($ast, 'definitions'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return EnumValueNode
*/
protected function buildEnum(array $ast): EnumValueNode
{
return new EnumValueNode(
$this->getValue($ast, 'value'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return EnumTypeDefinitionNode
* @throws LanguageException
*/
protected function buildEnumTypeDefinition(array $ast): EnumTypeDefinitionNode
{
return new EnumTypeDefinitionNode(
$this->buildNode($ast, 'description'),
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'directives'),
$this->buildNodes($ast, 'values'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return EnumTypeExtensionNode
* @throws LanguageException
*/
protected function buildEnumTypeExtension(array $ast): EnumTypeExtensionNode
{
return new EnumTypeExtensionNode(
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'directives'),
$this->buildNodes($ast, 'values'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return EnumValueDefinitionNode
* @throws LanguageException
*/
protected function buildEnumValueDefinition(array $ast): EnumValueDefinitionNode
{
return new EnumValueDefinitionNode(
$this->buildNode($ast, 'description'),
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'directives'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return FieldDefinitionNode
* @throws LanguageException
*/
protected function buildFieldDefinition(array $ast): FieldDefinitionNode
{
return new FieldDefinitionNode(
$this->buildNode($ast, 'description'),
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'arguments'),
$this->buildNode($ast, 'type'),
$this->buildNodes($ast, 'directives'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return FieldNode
* @throws LanguageException
*/
protected function buildField(array $ast): FieldNode
{
return new FieldNode(
$this->buildNode($ast, 'alias'),
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'arguments'),
$this->buildNodes($ast, 'directives'),
$this->buildNode($ast, 'selectionSet'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return FloatValueNode
*/
protected function buildFloat(array $ast): FloatValueNode
{
return new FloatValueNode(
$this->getValue($ast, 'value'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return FragmentDefinitionNode
* @throws LanguageException
*/
protected function buildFragmentDefinition(array $ast): FragmentDefinitionNode
{
return new FragmentDefinitionNode(
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'variableDefinitions'),
$this->buildNode($ast, 'typeCondition'),
$this->buildNodes($ast, 'directives'),
$this->buildNode($ast, 'selectionSet'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return FragmentSpreadNode
* @throws LanguageException
*/
protected function buildFragmentSpread(array $ast): FragmentSpreadNode
{
return new FragmentSpreadNode(
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'directives'),
$this->buildNode($ast, 'selectionSet'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return InlineFragmentNode
* @throws LanguageException
*/
protected function buildInlineFragment(array $ast): InlineFragmentNode
{
return new InlineFragmentNode(
$this->buildNode($ast, 'typeCondition'),
$this->buildNodes($ast, 'directives'),
$this->buildNode($ast, 'selectionSet'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return InputObjectTypeDefinitionNode
* @throws LanguageException
*/
protected function buildInputObjectTypeDefinition(array $ast): InputObjectTypeDefinitionNode
{
return new InputObjectTypeDefinitionNode(
$this->buildNode($ast, 'description'),
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'directives'),
$this->buildNodes($ast, 'fields'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return InputObjectTypeExtensionNode
* @throws LanguageException
*/
protected function buildInputObjectTypeExtension(array $ast): InputObjectTypeExtensionNode
{
return new InputObjectTypeExtensionNode(
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'directives'),
$this->buildNodes($ast, 'fields'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return InputValueDefinitionNode
* @throws LanguageException
*/
protected function buildInputValueDefinition(array $ast): InputValueDefinitionNode
{
return new InputValueDefinitionNode(
$this->buildNode($ast, 'description'),
$this->buildNode($ast, 'name'),
$this->buildNode($ast, 'type'),
$this->buildNode($ast, 'defaultValue'),
$this->buildNodes($ast, 'directives'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return InterfaceTypeDefinitionNode
* @throws LanguageException
*/
protected function buildInterfaceTypeDefinition(array $ast): InterfaceTypeDefinitionNode
{
return new InterfaceTypeDefinitionNode(
$this->buildNode($ast, 'description'),
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'directives'),
$this->buildNodes($ast, 'fields'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return InterfaceTypeExtensionNode
* @throws LanguageException
*/
protected function buildInterfaceTypeExtension(array $ast): InterfaceTypeExtensionNode
{
return new InterfaceTypeExtensionNode(
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'directives'),
$this->buildNodes($ast, 'fields'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return IntValueNode
*/
protected function buildInt(array $ast): IntValueNode
{
return new IntValueNode(
$this->getValue($ast, 'value'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return ListTypeNode
* @throws LanguageException
*/
protected function buildListType(array $ast): ListTypeNode
{
return new ListTypeNode(
$this->buildNode($ast, 'type'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return ListValueNode
* @throws LanguageException
*/
protected function buildList(array $ast): ListValueNode
{
return new ListValueNode(
$this->buildNodes($ast, 'values'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return NamedTypeNode
* @throws LanguageException
*/
protected function buildNamedType(array $ast): NamedTypeNode
{
return new NamedTypeNode(
$this->buildNode($ast, 'name'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return NameNode
*/
protected function buildName(array $ast): NameNode
{
return new NameNode(
$this->getValue($ast, 'value'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return NonNullTypeNode
* @throws LanguageException
*/
protected function buildNonNullType(array $ast): NonNullTypeNode
{
return new NonNullTypeNode(
$this->buildNode($ast, 'type'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return NullValueNode
*/
protected function buildNull(array $ast): NullValueNode
{
return new NullValueNode($this->createLocation($ast));
}
/**
* @param array $ast
* @return ObjectFieldNode
* @throws LanguageException
*/
protected function buildObjectField(array $ast): ObjectFieldNode
{
return new ObjectFieldNode(
$this->buildNode($ast, 'name'),
$this->buildNode($ast, 'value'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return ObjectTypeDefinitionNode
* @throws LanguageException
*/
protected function buildObjectTypeDefinition(array $ast): ObjectTypeDefinitionNode
{
return new ObjectTypeDefinitionNode(
$this->buildNode($ast, 'description'),
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'interfaces'),
$this->buildNodes($ast, 'directives'),
$this->buildNodes($ast, 'fields'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return ObjectTypeExtensionNode
* @throws LanguageException
*/
protected function buildObjectTypeExtension(array $ast): ObjectTypeExtensionNode
{
return new ObjectTypeExtensionNode(
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'interfaces'),
$this->buildNodes($ast, 'directives'),
$this->buildNodes($ast, 'fields'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return ObjectValueNode
* @throws LanguageException
*/
protected function buildObject(array $ast): ObjectValueNode
{
return new ObjectValueNode(
$this->buildNodes($ast, 'fields'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return OperationDefinitionNode
* @throws LanguageException
*/
protected function buildOperationDefinition(array $ast): OperationDefinitionNode
{
return new OperationDefinitionNode(
$this->getValue($ast, 'operation'),
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'variableDefinitions'),
$this->buildNodes($ast, 'directives'),
$this->buildNode($ast, 'selectionSet'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return OperationTypeDefinitionNode
* @throws LanguageException
*/
protected function buildOperationTypeDefinition(array $ast): OperationTypeDefinitionNode
{
return new OperationTypeDefinitionNode(
$this->getValue($ast, 'operation'),
$this->buildNode($ast, 'type'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return ScalarTypeDefinitionNode
* @throws LanguageException
*/
protected function buildScalarTypeDefinition(array $ast): ScalarTypeDefinitionNode
{
return new ScalarTypeDefinitionNode(
$this->buildNode($ast, 'description'),
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'directives'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return ScalarTypeExtensionNode
* @throws LanguageException
*/
protected function buildScalarTypeExtension(array $ast): ScalarTypeExtensionNode
{
return new ScalarTypeExtensionNode(
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'directives'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return SchemaDefinitionNode
* @throws LanguageException
*/
protected function buildSchemaDefinition(array $ast): SchemaDefinitionNode
{
return new SchemaDefinitionNode(
$this->buildNodes($ast, 'directives'),
$this->buildNodes($ast, 'operationTypes'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return SchemaExtensionNode
* @throws LanguageException
*/
protected function buildSchemaExtension(array $ast): SchemaExtensionNode
{
return new SchemaExtensionNode(
$this->buildNodes($ast, 'directives'),
$this->buildNodes($ast, 'operationTypes'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return SelectionSetNode
* @throws LanguageException
*/
protected function buildSelectionSet(array $ast): SelectionSetNode
{
return new SelectionSetNode(
$this->buildNodes($ast, 'selections'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return StringValueNode
*/
protected function buildString(array $ast): StringValueNode
{
return new StringValueNode(
$this->getValue($ast, 'value'),
$this->getValue($ast, 'block', false),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return UnionTypeDefinitionNode
* @throws LanguageException
*/
protected function buildUnionTypeDefinition(array $ast): UnionTypeDefinitionNode
{
return new UnionTypeDefinitionNode(
$this->buildNode($ast, 'description'),
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'directives'),
$this->buildNodes($ast, 'types'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return UnionTypeExtensionNode
* @throws LanguageException
*/
protected function buildUnionTypeExtension(array $ast): UnionTypeExtensionNode
{
return new UnionTypeExtensionNode(
$this->buildNode($ast, 'name'),
$this->buildNodes($ast, 'directives'),
$this->buildNodes($ast, 'types'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return VariableDefinitionNode
* @throws LanguageException
*/
protected function buildVariableDefinition(array $ast): VariableDefinitionNode
{
return new VariableDefinitionNode(
$this->buildNode($ast, 'variable'),
$this->buildNode($ast, 'type'),
$this->buildNode($ast, 'defaultValue'),
$this->createLocation($ast)
);
}
/**
* @param array $ast
* @return VariableNode
* @throws LanguageException
*/
protected function buildVariable(array $ast): VariableNode
{
return new VariableNode(
$this->buildNode($ast, 'name'),
$this->createLocation($ast)
);
}
/**
* Creates a location object.
*
* @param array $ast
* @return Location|null
*/
protected function createLocation(array $ast): ?Location
{
return isset($ast['loc']['start'], $ast['loc']['end'])
? new Location($ast['loc']['start'], $ast['loc']['end'], $ast['loc']['source'] ?? null)
: null;
}
/**
* Returns the value of a single property in the given AST.
*
* @param array $ast
* @param string $propertyName
* @param mixed $defaultValue
* @return mixed|null
*/
protected function getValue(array $ast, string $propertyName, $defaultValue = null)
{
return $ast[$propertyName] ?? $defaultValue;
}
/**
* Builds a single item from the given AST.
*
* @param array $ast
* @param string $propertyName
* @return NodeInterface|null
* @throws LanguageException
*/
protected function buildNode(array $ast, string $propertyName): ?NodeInterface
{
return isset($ast[$propertyName]) ? $this->build($ast[$propertyName]) : null;
}
/**
* Builds many items from the given AST.
*
* @param array $ast
* @param string $propertyName
* @return NodeInterface[]
* @throws LanguageException
*/
protected function buildNodes(array $ast, string $propertyName): array
{
$array = [];
if (isset($ast[$propertyName]) && \is_array($ast[$propertyName])) {
foreach ($ast[$propertyName] as $subAst) {
$array[] = $this->build($subAst);
}
}
return $array;
}
}
================================================
FILE: src/Language/NodeBuilderInterface.php
================================================
getKind();
if (\method_exists($this, $printMethod)) {
return $this->{$printMethod}($node);
}
throw new PrintException(\sprintf('Invalid AST Node: %s.', toString($node)));
}
/**
* @param NameNode $node
* @return null|string
*/
protected function printName(NameNode $node): ?string
{
return $node->getValue();
}
/**
* @param VariableNode $node
* @return string
*/
protected function printVariable(VariableNode $node): string
{
return '$' . $node->getName();
}
// Document
/**
* @param DocumentNode $node
* @return string
*/
protected function printDocument(DocumentNode $node): string
{
return \implode("\n\n", $node->getDefinitions()) . "\n";
}
/**
* @param OperationDefinitionNode $node
* @return string
* @throws PrintException
*/
protected function printOperationDefinition(OperationDefinitionNode $node): string
{
$operation = $node->getOperation();
$name = $this->printOne($node->getName());
$variablesDefinitions = $this->printMany($node->getVariableDefinitions());
$directives = $this->printMany($node->getDirectives());
$selectionSet = $this->printOne($node->getSelectionSet());
// Anonymous queries with no directives or variable definitions can use
// the query short form.
return empty($name) && empty($directives) && empty($variablesDefinitions) && $operation === 'query'
? $selectionSet
: \implode(' ', [
$operation,
$name . wrap('(', \implode(', ', $variablesDefinitions), ')'),
\implode(' ', $directives),
$selectionSet,
]);
}
/**
* @param VariableDefinitionNode $node
* @return string
* @throws PrintException
*/
protected function printVariableDefinition(VariableDefinitionNode $node): string
{
$variable = $this->printOne($node->getVariable());
$type = $this->printOne($node->getType());
$defaultValue = $this->printOne($node->getDefaultValue());
return $variable . ': ' . $type . wrap(' = ', $defaultValue);
}
/**
* @param SelectionSetNode $node
* @return string
*/
protected function printSelectionSet(SelectionSetNode $node): string
{
return block($this->printMany($node->getSelections()));
}
/**
* @param FieldNode $node
* @return string
* @throws PrintException
*/
protected function printField(FieldNode $node): string
{
$alias = $this->printOne($node->getAlias());
$name = $this->printOne($node->getName());
$arguments = $this->printMany($node->getArguments());
$directives = $this->printMany($node->getDirectives());
$selectionSet = $this->printOne($node->getSelectionSet());
return \implode(' ', [
wrap('', $alias, ': ') . $name . wrap('(', \implode(', ', $arguments), ')'),
\implode(' ', $directives),
$selectionSet,
]);
}
/**
* @param ArgumentNode $node
* @return string
* @throws PrintException
*/
protected function printArgument(ArgumentNode $node): string
{
$name = $this->printOne($node->getName());
$value = $this->printOne($node->getValue());
return $name . ': ' . $value;
}
// Fragments
/**
* @param FragmentSpreadNode $node
* @return string
* @throws PrintException
*/
protected function printFragmentSpread(FragmentSpreadNode $node): string
{
$name = $this->printOne($node->getName());
$directives = $this->printMany($node->getDirectives());
return '...' . $name . wrap(' ', \implode(' ', $directives));
}
/**
* @param InlineFragmentNode $node
* @return string
* @throws PrintException
*/
protected function printInlineFragment(InlineFragmentNode $node): string
{
$typeCondition = $this->printOne($node->getTypeCondition());
$directives = $this->printMany($node->getDirectives());
$selectionSet = $this->printOne($node->getSelectionSet());
return \implode(' ', [
'...', wrap('on ', $typeCondition),
\implode(' ', $directives),
$selectionSet
]);
}
/**
* @param FragmentDefinitionNode $node
* @return string
* @throws PrintException
*/
protected function printFragmentDefinition(FragmentDefinitionNode $node): string
{
$name = $this->printOne($node->getName());
$typeCondition = $this->printOne($node->getTypeCondition());
$variableDefinitions = $this->printMany($node->getVariableDefinitions());
$directives = $this->printMany($node->getDirectives());
$selectionSet = $this->printOne($node->getSelectionSet());
// Note: fragment variable definitions are experimental and may be changed
// or removed in the future.
return \implode(' ', [
'fragment ' . $name . wrap('(', \implode(', ', $variableDefinitions), ')'),
'on ' . $typeCondition . ' ' . \implode(' ', $directives),
$selectionSet
]);
}
// Value
/**
* @param IntValueNode $node
* @return string
*/
protected function printIntValue(IntValueNode $node): string
{
return (string)$node->getValue();
}
/**
* @param FloatValueNode $node
* @return string
*/
protected function printFloatValue(FloatValueNode $node): string
{
return (string)$node->getValue();
}
/**
* @param StringValueNode $node
* @return string
*/
protected function printStringValue(StringValueNode $node): string
{
$value = $node->getValue();
return $node->isBlock()
? printBlockString($value, false)
: \json_encode($value, JSON_UNESCAPED_UNICODE);
}
/**
* @param BooleanValueNode $node
* @return string
*/
protected function printBooleanValue(BooleanValueNode $node): string
{
return $node->getValue() ? 'true' : 'false';
}
/**
* @param NullValueNode $node
* @return string
*/
protected function printNullValue(NullValueNode $node): string
{
return 'null';
}
/**
* @param EnumValueNode $node
* @return string
*/
protected function printEnumValue(EnumValueNode $node): string
{
return (string)$node->getValue();
}
/**
* @param ListValueNode $node
* @return string
*/
protected function printListValue(ListValueNode $node): string
{
$values = $this->printMany($node->getValues());
return wrap('[', \implode(', ', $values), ']');
}
/**
* @param ObjectValueNode $node
* @return string
*/
protected function printObjectValue(ObjectValueNode $node): string
{
$fields = $this->printMany($node->getFields());
return wrap('{', \implode(', ', $fields), '}');
}
/**
* @param ObjectFieldNode $node
* @return string
* @throws PrintException
*/
protected function printObjectField(ObjectFieldNode $node): string
{
$name = $this->printOne($node->getName());
$value = $this->printOne($node->getValue());
return $name . ': ' . $value;
}
// Directive
/**
* @param DirectiveNode $node
* @return string
* @throws PrintException
*/
protected function printDirective(DirectiveNode $node): string
{
$name = $this->printOne($node->getName());
$arguments = $this->printMany($node->getArguments());
return '@' . $name . wrap('(', \implode(', ', $arguments), ')');
}
// Type
/**
* @param NamedTypeNode $node
* @return string
* @throws PrintException
*/
protected function printNamedType(NamedTypeNode $node): string
{
return $this->printOne($node->getName());
}
/**
* @param ListTypeNode $node
* @return string
* @throws PrintException
*/
protected function printListType(ListTypeNode $node): string
{
return wrap('[', $this->printOne($node->getType()), ']');
}
/**
* @param NonNullTypeNode $node
* @return string
* @throws PrintException
*/
protected function printNonNullType(NonNullTypeNode $node): string
{
return $this->printOne($node->getType()) . '!';
}
/**
* @param NodeInterface|null $node
* @return string
* @throws PrintException
*/
protected function printOne(?NodeInterface $node): string
{
return null !== $node ? $this->print($node) : '';
}
/**
* @param array $nodes
* @return array
*/
protected function printMany(array $nodes): array
{
return \array_map(function ($node) {
return $this->print($node);
}, $nodes);
}
}
================================================
FILE: src/Language/NodePrinterInterface.php
================================================
parsePartial([$this, $lexCallback], $arguments[0], $arguments[1] ?? []);
}
return $this;
}
/**
* Given a GraphQL source, parses it into a Document.
* Throws GraphQLError if a syntax error is encountered.
*
* @inheritdoc
* @throws SyntaxErrorException
* @throws \ReflectionException
* @throws InvariantException
*/
public function parse(Source $source, array $options = []): DocumentNode
{
$this->lexer = $this->createLexer($source, $options);
return $this->lexDocument();
}
/**
* @param callable $lexCallback
* @param Source $source
* @param array $options
* @return NodeInterface
* @throws SyntaxErrorException
*/
protected function parsePartial(callable $lexCallback, Source $source, array $options = []): NodeInterface
{
$this->lexer = $this->createLexer($source, $options);
$this->expect(TokenKindEnum::SOF);
$node = $lexCallback();
$this->expect(TokenKindEnum::EOF);
return $node;
}
/**
* Converts a name lex token into a name parse node.
*
* @return NameNode
* @throws SyntaxErrorException
*/
protected function lexName(): NameNode
{
$token = $this->expect(TokenKindEnum::NAME);
return new NameNode($token->getValue(), $this->createLocation($token));
}
// Implements the parsing rules in the Document section.
/**
* Document : Definition+
*
* @return DocumentNode
* @throws SyntaxErrorException
* @throws \ReflectionException
*/
protected function lexDocument(): DocumentNode
{
$start = $this->lexer->getToken();
$this->expect(TokenKindEnum::SOF);
$definitions = [];
do {
$definitions[] = $this->lexDefinition();
} while (!$this->skip(TokenKindEnum::EOF));
return new DocumentNode($definitions, $this->createLocation($start));
}
/**
* Definition :
* - ExecutableDefinition
* - TypeSystemDefinition
*
* @return NodeInterface
* @throws SyntaxErrorException
* @throws \ReflectionException
*/
protected function lexDefinition(): NodeInterface
{
if ($this->peek(TokenKindEnum::NAME)) {
$token = $this->lexer->getToken();
switch ($token->getValue()) {
case KeywordEnum::QUERY:
case KeywordEnum::MUTATION:
case KeywordEnum::SUBSCRIPTION:
case KeywordEnum::FRAGMENT:
return $this->lexExecutableDefinition();
case KeywordEnum::SCHEMA:
case KeywordEnum::SCALAR:
case KeywordEnum::TYPE:
case KeywordEnum::INTERFACE:
case KeywordEnum::UNION:
case KeywordEnum::ENUM:
case KeywordEnum::INPUT:
case KeywordEnum::DIRECTIVE:
return $this->lexTypeSystemDefinition();
case KeywordEnum::EXTEND:
return $this->lexTypeSystemExtension();
}
} elseif ($this->peek(TokenKindEnum::BRACE_L)) {
return $this->lexExecutableDefinition();
} elseif ($this->peekDescription()) {
return $this->lexTypeSystemDefinition();
}
throw $this->unexpected();
}
/**
* ExecutableDefinition :
* - OperationDefinition
* - FragmentDefinition
*
* @return ExecutableDefinitionNodeInterface
* @throws SyntaxErrorException
*/
protected function lexExecutableDefinition(): ExecutableDefinitionNodeInterface
{
if ($this->peek(TokenKindEnum::NAME)) {
// Valid names are: query, mutation, subscription and fragment
$token = $this->lexer->getToken();
switch ($token->getValue()) {
case KeywordEnum::QUERY:
case KeywordEnum::MUTATION:
case KeywordEnum::SUBSCRIPTION:
return $this->lexOperationDefinition();
case KeywordEnum::FRAGMENT:
return $this->lexFragmentDefinition();
}
} elseif ($this->peek(TokenKindEnum::BRACE_L)) {
// Anonymous query
return $this->lexOperationDefinition();
}
throw $this->unexpected();
}
// Implements the parsing rules in the Operations section.
/**
* OperationDefinition :
* - SelectionSet
* - OperationType Name? VariableDefinitions? Directives? SelectionSet
*
* @return OperationDefinitionNode
* @throws SyntaxErrorException
*/
protected function lexOperationDefinition(): OperationDefinitionNode
{
$start = $this->lexer->getToken();
if ($this->peek(TokenKindEnum::BRACE_L)) {
// Anonymous query
return new OperationDefinitionNode(
KeywordEnum::QUERY,
null,
[],
[],
$this->lexSelectionSet(),
$this->createLocation($start)
);
}
$operation = $this->lexOperationType();
if ($this->peek(TokenKindEnum::NAME)) {
$name = $this->lexName();
}
return new OperationDefinitionNode(
$operation,
$name ?? null,
$this->lexVariableDefinitions(),
$this->lexDirectives(),
$this->lexSelectionSet(),
$this->createLocation($start)
);
}
/**
* OperationType : one of query mutation subscription
*
* @return null|string
* @throws SyntaxErrorException
*/
protected function lexOperationType(): ?string
{
$token = $this->expect(TokenKindEnum::NAME);
$value = $token->getValue();
if (isOperation($value)) {
return $value;
}
throw $this->unexpected($token);
}
/**
* VariableDefinitions : ( VariableDefinition+ )
*
* @return array
* @throws SyntaxErrorException
*/
protected function lexVariableDefinitions(): array
{
return $this->peek(TokenKindEnum::PAREN_L)
? $this->many(
TokenKindEnum::PAREN_L,
[$this, 'lexVariableDefinition'],
TokenKindEnum::PAREN_R
)
: [];
}
/**
* VariableDefinition : Variable : Type DefaultValue?
*
* @return VariableDefinitionNode
* @throws SyntaxErrorException
*/
protected function lexVariableDefinition(): VariableDefinitionNode
{
$start = $this->lexer->getToken();
/**
* @return TypeNodeInterface
*/
$parseType = function (): TypeNodeInterface {
$this->expect(TokenKindEnum::COLON);
return $this->lexType();
};
return new VariableDefinitionNode(
$this->lexVariable(),
$parseType(),
$this->skip(TokenKindEnum::EQUALS)
? $this->lexValue(true)
: null,
$this->createLocation($start)
);
}
/**
* Variable : $ Name
*
* @return VariableNode
* @throws SyntaxErrorException
*/
protected function lexVariable(): VariableNode
{
$start = $this->lexer->getToken();
$this->expect(TokenKindEnum::DOLLAR);
return new VariableNode($this->lexName(), $this->createLocation($start));
}
/**
* SelectionSet : { Selection+ }
*
* @return SelectionSetNode
* @throws SyntaxErrorException
*/
protected function lexSelectionSet(): SelectionSetNode
{
$start = $this->lexer->getToken();
return new SelectionSetNode(
$this->many(
TokenKindEnum::BRACE_L,
[$this, 'lexSelection'],
TokenKindEnum::BRACE_R
),
$this->createLocation($start)
);
}
/**
* Selection :
* - Field
* - FragmentSpread
* - InlineFragment
*
* @return NodeInterface|FragmentNodeInterface|FieldNode
* @throws SyntaxErrorException
*/
protected function lexSelection(): NodeInterface
{
return $this->peek(TokenKindEnum::SPREAD)
? $this->lexFragment()
: $this->lexField();
}
/**
* Field : Alias? Name Arguments? Directives? SelectionSet?
*
* Alias : Name :
*
* @return FieldNode
* @throws SyntaxErrorException
*/
protected function lexField(): FieldNode
{
$start = $this->lexer->getToken();
$nameOrAlias = $this->lexName();
if ($this->skip(TokenKindEnum::COLON)) {
$alias = $nameOrAlias;
$name = $this->lexName();
} else {
$name = $nameOrAlias;
}
return new FieldNode(
$alias ?? null,
$name,
$this->lexArguments(false),
$this->lexDirectives(),
$this->peek(TokenKindEnum::BRACE_L)
? $this->lexSelectionSet()
: null,
$this->createLocation($start)
);
}
/**
* Arguments[Const] : ( Argument[?Const]+ )
*
* @param bool $isConst
* @return array
* @throws SyntaxErrorException
*/
protected function lexArguments(bool $isConst = false): ?array
{
/**
* @return ArgumentNode
*/
$parseFunction = function () use ($isConst): ArgumentNode {
return $this->lexArgument($isConst);
};
return $this->peek(TokenKindEnum::PAREN_L)
? $this->many(
TokenKindEnum::PAREN_L,
$parseFunction,
TokenKindEnum::PAREN_R
)
: [];
}
/**
* Argument[Const] : Name : Value[?Const]
*
* @param bool $isConst
* @return ArgumentNode
* @throws SyntaxErrorException
*/
protected function lexArgument(bool $isConst = false): ArgumentNode
{
$start = $this->lexer->getToken();
/**
* @return NodeInterface|TypeNodeInterface|ValueNodeInterface
*/
$parseValue = function () use ($isConst): NodeInterface {
$this->expect(TokenKindEnum::COLON);
return $this->lexValue($isConst);
};
return new ArgumentNode(
$this->lexName(),
$parseValue(),
$this->createLocation($start)
);
}
// Implements the parsing rules in the Fragments section.
/**
* Corresponds to both FragmentSpread and InlineFragment in the spec.
*
* FragmentSpread : ... FragmentName Directives?
*
* InlineFragment : ... TypeCondition? Directives? SelectionSet
*
* @return FragmentNodeInterface
* @throws SyntaxErrorException
*/
protected function lexFragment(): FragmentNodeInterface
{
$start = $this->lexer->getToken();
$this->expect(TokenKindEnum::SPREAD);
$token = $this->lexer->getToken();
if (KeywordEnum::ON !== $token->getValue() && $this->peek(TokenKindEnum::NAME)) {
return new FragmentSpreadNode(
$this->lexFragmentName($token),
$this->lexDirectives(),
null,
$this->createLocation($start)
);
}
if (KeywordEnum::ON === $token->getValue()) {
$this->lexer->advance();
$typeCondition = $this->lexNamedType();
}
return new InlineFragmentNode(
$typeCondition ?? null,
$this->lexDirectives(),
$this->lexSelectionSet(),
$this->createLocation($start)
);
}
/**
* FragmentDefinition :
* - fragment FragmentName on TypeCondition Directives? SelectionSet
*
* TypeCondition : NamedType
*
* @return FragmentDefinitionNode
* @throws SyntaxErrorException
*/
protected function lexFragmentDefinition(): FragmentDefinitionNode
{
$start = $this->lexer->getToken();
$this->expectKeyword(KeywordEnum::FRAGMENT);
$parseTypeCondition = function () {
$this->expectKeyword(KeywordEnum::ON);
return $this->lexNamedType();
};
return new FragmentDefinitionNode(
$this->lexFragmentName(),
$this->lexVariableDefinitions(),
$parseTypeCondition(),
$this->lexDirectives(),
$this->lexSelectionSet(),
$this->createLocation($start)
);
}
/**
* FragmentName : Name but not `on`
*
* @param Token|null $token
* @return NameNode
* @throws SyntaxErrorException
*/
protected function lexFragmentName(?Token $token = null): NameNode
{
if (null === $token) {
$token = $this->lexer->getToken();
}
if (KeywordEnum::ON === $token->getValue()) {
throw $this->unexpected();
}
return $this->lexName();
}
// Implements the parsing rules in the Values section.
/**
* Value[Const] :
* - [~Const] Variable
* - IntValue
* - FloatValue
* - StringValue
* - BooleanValue
* - NullValue
* - EnumValue
* - ListValue[?Const]
* - ObjectValue[?Const]
*
* BooleanValue : one of `true` `false`
*
* NullValue : `null`
*
* EnumValue : Name but not `true`, `false` or `null`
*
* @param bool $isConst
* @return NodeInterface|ValueNodeInterface|TypeNodeInterface
* @throws SyntaxErrorException
*/
protected function lexValue(bool $isConst = false): NodeInterface
{
$token = $this->lexer->getToken();
$value = $token->getValue();
switch ($token->getKind()) {
case TokenKindEnum::BRACKET_L:
return $this->lexList($isConst);
case TokenKindEnum::BRACE_L:
return $this->lexObject($isConst);
case TokenKindEnum::INT:
$this->lexer->advance();
return new IntValueNode($value, $this->createLocation($token));
case TokenKindEnum::FLOAT:
$this->lexer->advance();
return new FloatValueNode($value, $this->createLocation($token));
case TokenKindEnum::STRING:
case TokenKindEnum::BLOCK_STRING:
return $this->lexStringLiteral();
case TokenKindEnum::NAME:
if ($value === 'true' || $value === 'false') {
$this->lexer->advance();
return new BooleanValueNode($value === 'true', $this->createLocation($token));
}
if ($value === 'null') {
$this->lexer->advance();
return new NullValueNode($this->createLocation($token));
}
$this->lexer->advance();
return new EnumValueNode($value, $this->createLocation($token));
case TokenKindEnum::DOLLAR:
if (!$isConst) {
return $this->lexVariable();
}
break;
}
throw $this->unexpected();
}
/**
* @return StringValueNode
*/
protected function lexStringLiteral(): StringValueNode
{
$token = $this->lexer->getToken();
$this->lexer->advance();
return new StringValueNode(
$token->getValue(),
TokenKindEnum::BLOCK_STRING === $token->getKind(),
$this->createLocation($token)
);
}
/**
* ListValue[Const] :
* - [ ]
* - [ Value[?Const]+ ]
*
* @param bool $isConst
* @return ListValueNode
* @throws SyntaxErrorException
*/
protected function lexList(bool $isConst): ListValueNode
{
$start = $this->lexer->getToken();
$parseFunction = function () use ($isConst) {
return $this->lexValue($isConst);
};
return new ListValueNode(
$this->any(
TokenKindEnum::BRACKET_L,
$parseFunction,
TokenKindEnum::BRACKET_R
),
$this->createLocation($start)
);
}
/**
* ObjectValue[Const] :
* - { }
* - { ObjectField[?Const]+ }
*
* @param bool $isConst
* @return ObjectValueNode
* @throws SyntaxErrorException
*/
protected function lexObject(bool $isConst): ObjectValueNode
{
$start = $this->lexer->getToken();
$this->expect(TokenKindEnum::BRACE_L);
$fields = [];
while (!$this->skip(TokenKindEnum::BRACE_R)) {
$fields[] = $this->lexObjectField($isConst);
}
return new ObjectValueNode($fields, $this->createLocation($start));
}
/**
* ObjectField[Const] : Name : Value[?Const]
*
* @param bool $isConst
* @return ObjectFieldNode
* @throws SyntaxErrorException
*/
protected function lexObjectField(bool $isConst): ObjectFieldNode
{
$start = $this->lexer->getToken();
/**
* @param bool $isConst
* @return NodeInterface|TypeNodeInterface|ValueNodeInterface
*/
$parseValue = function (bool $isConst): NodeInterface {
$this->expect(TokenKindEnum::COLON);
return $this->lexValue($isConst);
};
return new ObjectFieldNode(
$this->lexName(),
$parseValue($isConst),
$this->createLocation($start)
);
}
// Implements the parsing rules in the Directives section.
/**
* Directives[Const] : Directive[?Const]+
*
* @param bool $isConst
* @return array
* @throws SyntaxErrorException
*/
protected function lexDirectives(bool $isConst = false): array
{
$directives = [];
while ($this->peek(TokenKindEnum::AT)) {
$directives[] = $this->lexDirective($isConst);
}
return $directives;
}
/**
* Directive[Const] : @ Name Arguments[?Const]?
*
* @param bool $isConst
* @return DirectiveNode
* @throws SyntaxErrorException
*/
protected function lexDirective(bool $isConst = false): DirectiveNode
{
$start = $this->lexer->getToken();
$this->expect(TokenKindEnum::AT);
return new DirectiveNode(
$this->lexName(),
$this->lexArguments($isConst),
$this->createLocation($start)
);
}
// Implements the parsing rules in the Types section.
/**
* Type :
* - NamedType
* - ListType
* - NonNullType
*
* @return TypeNodeInterface
* @throws SyntaxErrorException
*/
protected function lexType(): TypeNodeInterface
{
$start = $this->lexer->getToken();
if ($this->skip(TokenKindEnum::BRACKET_L)) {
$type = $this->lexType();
$this->expect(TokenKindEnum::BRACKET_R);
$type = new ListTypeNode($type, $this->createLocation($start));
} else {
$type = $this->lexNamedType();
}
if ($this->skip(TokenKindEnum::BANG)) {
return new NonNullTypeNode($type, $this->createLocation($start));
}
return $type;
}
/**
* NamedType : Name
*
* @return NamedTypeNode
* @throws SyntaxErrorException
*/
protected function lexNamedType(): NamedTypeNode
{
$start = $this->lexer->getToken();
return new NamedTypeNode($this->lexName(), $this->createLocation($start));
}
// Implements the parsing rules in the Type Definition section.
/**
* TypeSystemDefinition :
* - SchemaDefinition
* - TypeDefinition
* - TypeExtension
* - DirectiveDefinition
*
* TypeDefinition :
* - ScalarTypeDefinition
* - ObjectTypeDefinition
* - InterfaceTypeDefinition
* - UnionTypeDefinition
* - EnumTypeDefinition
* - InputObjectTypeDefinition
*
* @return TypeSystemDefinitionNodeInterface
* @throws SyntaxErrorException
* @throws \ReflectionException
*/
protected function lexTypeSystemDefinition(): TypeSystemDefinitionNodeInterface
{
// Many definitions begin with a description and require a lookahead.
$token = $this->peekDescription()
? $this->lexer->lookahead()
: $this->lexer->getToken();
if (TokenKindEnum::NAME === $token->getKind()) {
switch ($token->getValue()) {
case KeywordEnum::SCHEMA:
return $this->lexSchemaDefinition();
case KeywordEnum::SCALAR:
return $this->lexScalarTypeDefinition();
case KeywordEnum::TYPE:
return $this->lexObjectTypeDefinition();
case KeywordEnum::INTERFACE:
return $this->lexInterfaceTypeDefinition();
case KeywordEnum::UNION:
return $this->lexUnionTypeDefinition();
case KeywordEnum::ENUM:
return $this->lexEnumTypeDefinition();
case KeywordEnum::INPUT:
return $this->lexInputObjectTypeDefinition();
case KeywordEnum::DIRECTIVE:
return $this->lexDirectiveDefinition();
}
}
throw $this->unexpected($token);
}
/**
* @return bool
*/
protected function peekDescription(): bool
{
return $this->peek(TokenKindEnum::STRING) || $this->peek(TokenKindEnum::BLOCK_STRING);
}
/**
* Description : StringValue
*
* @return StringValueNode|null
*/
public function lexDescription(): ?StringValueNode
{
return $this->peekDescription()
? $this->lexStringLiteral()
: null;
}
/**
* SchemaDefinition : schema Directives[Const]? { OperationTypeDefinition+ }
*
* @return SchemaDefinitionNode
* @throws SyntaxErrorException
*/
protected function lexSchemaDefinition(): SchemaDefinitionNode
{
$start = $this->lexer->getToken();
$this->expectKeyword(KeywordEnum::SCHEMA);
return new SchemaDefinitionNode(
$this->lexDirectives(),
$this->many(
TokenKindEnum::BRACE_L,
[$this, 'lexOperationTypeDefinition'],
TokenKindEnum::BRACE_R
),
$this->createLocation($start)
);
}
/**
* OperationTypeDefinition : OperationType : NamedType
*
* @return OperationTypeDefinitionNode
* @throws SyntaxErrorException
*/
protected function lexOperationTypeDefinition(): OperationTypeDefinitionNode
{
$start = $this->lexer->getToken();
$operation = $this->lexOperationType();
$this->expect(TokenKindEnum::COLON);
return new OperationTypeDefinitionNode(
$operation,
$this->lexNamedType(),
$this->createLocation($start)
);
}
/**
* ScalarTypeDefinition : Description? scalar Name Directives[Const]?
*
* @return ScalarTypeDefinitionNode
* @throws SyntaxErrorException
*/
protected function lexScalarTypeDefinition(): ScalarTypeDefinitionNode
{
$start = $this->lexer->getToken();
$description = $this->lexDescription();
$this->expectKeyword(KeywordEnum::SCALAR);
return new ScalarTypeDefinitionNode(
$description,
$this->lexName(),
$this->lexDirectives(),
$this->createLocation($start)
);
}
/**
* ObjectTypeDefinition :
* Description?
* type Name ImplementsInterfaces? Directives[Const]? FieldsDefinition?
*
* @return ObjectTypeDefinitionNode
* @throws SyntaxErrorException
*/
protected function lexObjectTypeDefinition(): ObjectTypeDefinitionNode
{
$start = $this->lexer->getToken();
$description = $this->lexDescription();
$this->expectKeyword(KeywordEnum::TYPE);
return new ObjectTypeDefinitionNode(
$description,
$this->lexName(),
$this->lexImplementsInterfaces(),
$this->lexDirectives(),
$this->lexFieldsDefinition(),
$this->createLocation($start)
);
}
/**
* ImplementsInterfaces :
* - implements `&`? NamedType
* - ImplementsInterfaces & NamedType
*
* @return array
* @throws SyntaxErrorException
*/
protected function lexImplementsInterfaces(): array
{
$types = [];
$token = $this->lexer->getToken();
if ('implements' === $token->getValue()) {
$this->lexer->advance();
// Optional leading ampersand
$this->skip(TokenKindEnum::AMP);
do {
$types[] = $this->lexNamedType();
} while ($this->skip(TokenKindEnum::AMP));
}
return $types;
}
/**
* FieldsDefinition : { FieldDefinition+ }
*
* @return array
* @throws SyntaxErrorException
*/
protected function lexFieldsDefinition(): array
{
return $this->peek(TokenKindEnum::BRACE_L)
? $this->many(
TokenKindEnum::BRACE_L,
[$this, 'lexFieldDefinition'],
TokenKindEnum::BRACE_R
)
: [];
}
/**
* FieldDefinition :
* - Description? Name ArgumentsDefinition? : Type Directives[Const]?
*
* @return FieldDefinitionNode
* @throws SyntaxErrorException
*/
protected function lexFieldDefinition(): FieldDefinitionNode
{
$start = $this->lexer->getToken();
$description = $this->lexDescription();
$name = $this->lexName();
$arguments = $this->lexArgumentsDefinition();
$this->expect(TokenKindEnum::COLON);
return new FieldDefinitionNode(
$description,
$name,
$arguments,
$this->lexType(),
$this->lexDirectives(),
$this->createLocation($start)
);
}
/**
* ArgumentsDefinition : ( InputValueDefinition+ )
*
* @return InputValueDefinitionNode[]
* @throws SyntaxErrorException
*/
protected function lexArgumentsDefinition(): array
{
$parseFunction = function (): InputValueDefinitionNode {
return $this->lexInputValueDefinition();
};
return $this->peek(TokenKindEnum::PAREN_L)
? $this->many(
TokenKindEnum::PAREN_L,
$parseFunction,
TokenKindEnum::PAREN_R
)
: [];
}
/**
* InputValueDefinition :
* - Description? Name : Type DefaultValue? Directives[Const]?
*
* @return InputValueDefinitionNode
* @throws SyntaxErrorException
*/
protected function lexInputValueDefinition(): InputValueDefinitionNode
{
$start = $this->lexer->getToken();
$description = $this->lexDescription();
$name = $this->lexName();
$this->expect(TokenKindEnum::COLON);
return new InputValueDefinitionNode(
$description,
$name,
$this->lexType(),
$this->skip(TokenKindEnum::EQUALS)
? $this->lexValue(true)
: null,
$this->lexDirectives(true),
$this->createLocation($start)
);
}
/**
* InterfaceTypeDefinition :
* - Description? interface Name Directives[Const]? FieldsDefinition?
*
* @return InterfaceTypeDefinitionNode
* @throws SyntaxErrorException
*/
protected function lexInterfaceTypeDefinition(): InterfaceTypeDefinitionNode
{
$start = $this->lexer->getToken();
$description = $this->lexDescription();
$this->expectKeyword(KeywordEnum::INTERFACE);
return new InterfaceTypeDefinitionNode(
$description,
$this->lexName(),
$this->lexDirectives(),
$this->lexFieldsDefinition(),
$this->createLocation($start)
);
}
/**
* UnionTypeDefinition :
* - Description? union Name Directives[Const]? UnionMemberTypes?
*
* @return UnionTypeDefinitionNode
* @throws SyntaxErrorException
*/
protected function lexUnionTypeDefinition(): UnionTypeDefinitionNode
{
$start = $this->lexer->getToken();
$description = $this->lexDescription();
$this->expectKeyword(KeywordEnum::UNION);
return new UnionTypeDefinitionNode(
$description,
$this->lexName(),
$this->lexDirectives(),
$this->lexUnionMemberTypes(),
$this->createLocation($start)
);
}
/**
* UnionMemberTypes :
* - = `|`? NamedType
* - UnionMemberTypes | NamedType
*
* @return array
* @throws SyntaxErrorException
*/
protected function lexUnionMemberTypes(): array
{
$types = [];
if ($this->skip(TokenKindEnum::EQUALS)) {
// Optional leading pipe
$this->skip(TokenKindEnum::PIPE);
do {
$types[] = $this->lexNamedType();
} while ($this->skip(TokenKindEnum::PIPE));
}
return $types;
}
/**
* EnumTypeDefinition :
* - Description? enum Name Directives[Const]? EnumValuesDefinition?
*
* @return EnumTypeDefinitionNode
* @throws SyntaxErrorException
*/
protected function lexEnumTypeDefinition(): EnumTypeDefinitionNode
{
$start = $this->lexer->getToken();
$description = $this->lexDescription();
$this->expectKeyword(KeywordEnum::ENUM);
return new EnumTypeDefinitionNode(
$description,
$this->lexName(),
$this->lexDirectives(),
$this->lexEnumValuesDefinition(),
$this->createLocation($start)
);
}
/**
* EnumValuesDefinition : { EnumValueDefinition+ }
*
* @return array
* @throws SyntaxErrorException
*/
protected function lexEnumValuesDefinition(): array
{
return $this->peek(TokenKindEnum::BRACE_L)
? $this->many(
TokenKindEnum::BRACE_L,
[$this, 'lexEnumValueDefinition'],
TokenKindEnum::BRACE_R
)
: [];
}
/**
* EnumValueDefinition : Description? EnumValue Directives[Const]?
*
* EnumValue : Name
*
* @return EnumValueDefinitionNode
* @throws SyntaxErrorException
*/
protected function lexEnumValueDefinition(): EnumValueDefinitionNode
{
$start = $this->lexer->getToken();
return new EnumValueDefinitionNode(
$this->lexDescription(),
$this->lexName(),
$this->lexDirectives(),
$this->createLocation($start)
);
}
/**
* InputObjectTypeDefinition :
* - Description? input Name Directives[Const]? InputFieldsDefinition?
*
* @return InputObjectTypeDefinitionNode
* @throws SyntaxErrorException
*/
protected function lexInputObjectTypeDefinition(): InputObjectTypeDefinitionNode
{
$start = $this->lexer->getToken();
$description = $this->lexDescription();
$this->expectKeyword(KeywordEnum::INPUT);
return new InputObjectTypeDefinitionNode(
$description,
$this->lexName(),
$this->lexDirectives(true),
$this->lexInputFieldsDefinition(),
$this->createLocation($start)
);
}
/**
* InputFieldsDefinition : { InputValueDefinition+ }
*
* @return array
* @throws SyntaxErrorException
*/
protected function lexInputFieldsDefinition(): array
{
$parseFunction = function (): InputValueDefinitionNode {
return $this->lexInputValueDefinition();
};
return $this->peek(TokenKindEnum::BRACE_L)
? $this->many(
TokenKindEnum::BRACE_L,
$parseFunction,
TokenKindEnum::BRACE_R
)
: [];
}
/**
* TypeExtension :
* - ScalarTypeExtension
* - ObjectTypeExtension
* - InterfaceTypeExtension
* - UnionTypeExtension
* - EnumTypeExtension
* - InputObjectTypeDefinition
*
* @return TypeSystemExtensionNodeInterface
* @throws SyntaxErrorException
*/
protected function lexTypeSystemExtension(): TypeSystemExtensionNodeInterface
{
$token = $this->lexer->lookahead();
if (TokenKindEnum::NAME === $token->getKind()) {
switch ($token->getValue()) {
case KeywordEnum::SCHEMA:
return $this->lexSchemaExtension();
case KeywordEnum::SCALAR:
return $this->lexScalarTypeExtension(false);
case KeywordEnum::TYPE:
return $this->lexObjectTypeExtension();
case KeywordEnum::INTERFACE:
return $this->lexInterfaceTypeExtension();
case KeywordEnum::UNION:
return $this->lexUnionTypeExtension();
case KeywordEnum::ENUM:
return $this->lexEnumTypeExtension();
case KeywordEnum::INPUT:
return $this->lexInputObjectTypeExtension();
}
}
throw $this->unexpected($token);
}
/**
* SchemaExtension :
* - extend schema Directives[Const]? { OperationTypeDefinition+ }
* - extend schema Directives[Const]
*
* @return SchemaExtensionNode
* @throws SyntaxErrorException
*/
protected function lexSchemaExtension(): SchemaExtensionNode
{
$start = $this->lexer->getToken();
$this->expectKeyword(KeywordEnum::EXTEND);
$this->expectKeyword(KeywordEnum::SCHEMA);
$directives = $this->lexDirectives(true);
$parseFunction = function (): OperationTypeDefinitionNode {
return $this->lexOperationTypeDefinition();
};
$operationTypes = $this->peek(TokenKindEnum::BRACE_L)
? $this->many(
TokenKindEnum::BRACE_L,
$parseFunction,
TokenKindEnum::BRACE_R
)
: [];
if (empty($directives) && empty($operationTypes)) {
$this->unexpected();
}
return new SchemaExtensionNode($directives, $operationTypes, $this->createLocation($start));
}
/**
* ScalarTypeExtension :
* - extend scalar Name Directives[Const]
*
* @param bool $isConst
* @return ScalarTypeExtensionNode
* @throws SyntaxErrorException
*/
protected function lexScalarTypeExtension(bool $isConst = false): ScalarTypeExtensionNode
{
$start = $this->lexer->getToken();
$this->expectKeyword(KeywordEnum::EXTEND);
$this->expectKeyword(KeywordEnum::SCALAR);
$name = $this->lexName();
$directives = $this->lexDirectives($isConst);
if (empty($directives)) {
throw $this->unexpected();
}
return new ScalarTypeExtensionNode($name, $directives, $this->createLocation($start));
}
/**
* ObjectTypeExtension :
* - extend type Name ImplementsInterfaces? Directives[Const]? FieldsDefinition
* - extend type Name ImplementsInterfaces? Directives[Const]
* - extend type Name ImplementsInterfaces
*
* @return ObjectTypeExtensionNode
* @throws SyntaxErrorException
*/
protected function lexObjectTypeExtension(): ObjectTypeExtensionNode
{
$start = $this->lexer->getToken();
$this->expectKeyword(KeywordEnum::EXTEND);
$this->expectKeyword(KeywordEnum::TYPE);
$name = $this->lexName();
$interfaces = $this->lexImplementsInterfaces();
$directives = $this->lexDirectives();
$fields = $this->lexFieldsDefinition();
if (empty($interfaces) && empty($directives) && empty($fields)) {
throw $this->unexpected();
}
return new ObjectTypeExtensionNode(
$name,
$interfaces,
$directives,
$fields,
$this->createLocation($start)
);
}
/**
* InterfaceTypeExtension :
* - extend interface Name Directives[Const]? FieldsDefinition
* - extend interface Name Directives[Const]
*
* @return InterfaceTypeExtensionNode
* @throws SyntaxErrorException
*/
protected function lexInterfaceTypeExtension(): InterfaceTypeExtensionNode
{
$start = $this->lexer->getToken();
$this->expectKeyword(KeywordEnum::EXTEND);
$this->expectKeyword(KeywordEnum::INTERFACE);
$name = $this->lexName();
$directives = $this->lexDirectives();
$fields = $this->lexFieldsDefinition();
if (empty($directives) && empty($fields)) {
throw $this->unexpected();
}
return new InterfaceTypeExtensionNode($name, $directives, $fields, $this->createLocation($start));
}
/**
* UnionTypeExtension :
* - extend union Name Directives[Const]? UnionMemberTypes
* - extend union Name Directives[Const]
*
* @return UnionTypeExtensionNode
* @throws SyntaxErrorException
*/
protected function lexUnionTypeExtension(): UnionTypeExtensionNode
{
$start = $this->lexer->getToken();
$this->expectKeyword(KeywordEnum::EXTEND);
$this->expectKeyword(KeywordEnum::UNION);
$name = $this->lexName();
$directives = $this->lexDirectives();
$types = $this->lexUnionMemberTypes();
if (empty($directives) && empty($types)) {
throw $this->unexpected();
}
return new UnionTypeExtensionNode($name, $directives, $types, $this->createLocation($start));
}
/**
* EnumTypeExtension :
* - extend enum Name Directives[Const]? EnumValuesDefinition
* - extend enum Name Directives[Const]
*
* @return EnumTypeExtensionNode
* @throws SyntaxErrorException
*/
protected function lexEnumTypeExtension(): EnumTypeExtensionNode
{
$start = $this->lexer->getToken();
$this->expectKeyword(KeywordEnum::EXTEND);
$this->expectKeyword(KeywordEnum::ENUM);
$name = $this->lexName();
$directives = $this->lexDirectives();
$values = $this->lexEnumValuesDefinition();
if (empty($directives) && empty($values)) {
throw $this->unexpected();
}
return new EnumTypeExtensionNode($name, $directives, $values, $this->createLocation($start));
}
/**
* InputObjectTypeExtension :
* - extend input Name Directives[Const]? InputFieldsDefinition
* - extend input Name Directives[Const]
*
* @return InputObjectTypeExtensionNode
* @throws SyntaxErrorException
*/
protected function lexInputObjectTypeExtension(): InputObjectTypeExtensionNode
{
$start = $this->lexer->getToken();
$this->expectKeyword(KeywordEnum::EXTEND);
$this->expectKeyword(KeywordEnum::INPUT);
$name = $this->lexName();
$directives = $this->lexDirectives(true);
$fields = $this->lexInputFieldsDefinition();
if (empty($directives) && empty($fields)) {
throw $this->unexpected();
}
return new InputObjectTypeExtensionNode($name, $directives, $fields, $this->createLocation($start));
}
/**
* DirectiveDefinition :
* - Description? directive @ Name ArgumentsDefinition? on DirectiveLocations
*
* @return DirectiveDefinitionNode
* @throws SyntaxErrorException
* @throws \ReflectionException
*/
protected function lexDirectiveDefinition(): DirectiveDefinitionNode
{
$start = $this->lexer->getToken();
$description = $this->lexDescription();
$this->expectKeyword(KeywordEnum::DIRECTIVE);
$this->expect(TokenKindEnum::AT);
$name = $this->lexName();
$arguments = $this->lexArgumentsDefinition();
$this->expectKeyword(KeywordEnum::ON);
$locations = $this->lexDirectiveLocations();
return new DirectiveDefinitionNode(
$description,
$name,
$arguments,
$locations,
$this->createLocation($start)
);
}
/**
* DirectiveLocations :
* - `|`? DirectiveLocation
* - DirectiveLocations | DirectiveLocation
*
* @return array
* @throws SyntaxErrorException
* @throws \ReflectionException
*/
protected function lexDirectiveLocations(): array
{
$this->skip(TokenKindEnum::PIPE);
$locations = [];
do {
$locations[] = $this->lexDirectiveLocation();
} while ($this->skip(TokenKindEnum::PIPE));
return $locations;
}
/**
* DirectiveLocation :
* - ExecutableDirectiveLocation
* - TypeSystemDirectiveLocation
*
* ExecutableDirectiveLocation : one of
* `QUERY`
* `MUTATION`
* `SUBSCRIPTION`
* `FIELD`
* `FRAGMENT_DEFINITION`
* `FRAGMENT_SPREAD`
* `INLINE_FRAGMENT`
*
* TypeSystemDirectiveLocation : one of
* `SCHEMA`
* `SCALAR`
* `OBJECT`
* `FIELD_DEFINITION`
* `ARGUMENT_DEFINITION`
* `INTERFACE`
* `UNION`
* `ENUM`
* `ENUM_VALUE`
* `INPUT_OBJECT`
* `INPUT_FIELD_DEFINITION`
*
* @return NameNode
* @throws SyntaxErrorException
* @throws \ReflectionException
*/
protected function lexDirectiveLocation(): NameNode
{
$start = $this->lexer->getToken();
$name = $this->lexName();
if (arraySome(DirectiveLocationEnum::values(), function ($value) use ($name) {
return $name->getValue() === $value;
})) {
return $name;
}
throw $this->unexpected($start);
}
/**
* Returns a location object, used to identify the place in
* the source that created a given parsed object.
*
* @param Token $start
* @return Location|null
*/
protected function createLocation(Token $start): ?Location
{
return !$this->lexer->getOption('noLocation', false)
? new Location(
$start->getStart(),
$this->lexer->getLastToken()->getEnd(),
$this->lexer->getSource()
)
: null;
}
/**
* @param Source $source
* @param array $options
* @return LexerInterface
*/
protected function createLexer($source, array $options): LexerInterface
{
return new Lexer($source, $options);
}
/**
* Determines if the next token is of a given kind.
*
* @param string $kind
* @return bool
*/
protected function peek(string $kind): bool
{
return $kind === $this->lexer->getToken()->getKind();
}
/**
* If the next token is of the given kind, return true after advancing
* the lexer. Otherwise, do not change the parser state and return false.
*
* @param string $kind
* @return bool
*/
protected function skip(string $kind): bool
{
if ($match = $this->peek($kind)) {
$this->lexer->advance();
}
return $match;
}
/**
* If the next token is of the given kind, return that token after advancing
* the lexer. Otherwise, do not change the parser state and throw an error.
*
* @param string $kind
* @return Token
* @throws SyntaxErrorException
*/
protected function expect(string $kind): Token
{
$token = $this->lexer->getToken();
if ($kind === $token->getKind()) {
$this->lexer->advance();
return $token;
}
throw $this->lexer->createSyntaxErrorException(\sprintf('Expected %s, found %s.', $kind, $token));
}
/**
* @param string $value
* @return Token
* @throws SyntaxErrorException
*/
protected function expectKeyword(string $value): Token
{
$token = $this->lexer->getToken();
if (TokenKindEnum::NAME === $token->getKind() && $value === $token->getValue()) {
$this->lexer->advance();
return $token;
}
throw $this->lexer->createSyntaxErrorException(\sprintf('Expected %s, found %s', $value, $token));
}
/**
* Helper function for creating an error when an unexpected lexed token
* is encountered.
*
* @param Token|null $atToken
* @return SyntaxErrorException
*/
protected function unexpected(?Token $atToken = null): SyntaxErrorException
{
$token = $atToken ?? $this->lexer->getToken();
return $this->lexer->createSyntaxErrorException(\sprintf('Unexpected %s', $token));
}
/**
* Returns a possibly empty list of parse nodes, determined by
* the parseFn. This list begins with a lex token of openKind
* 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 $parseFunction
* @param string $closeKind
* @return array
* @throws SyntaxErrorException
*/
protected function any(string $openKind, callable $parseFunction, string $closeKind): array
{
$this->expect($openKind);
$nodes = [];
while (!$this->skip($closeKind)) {
$nodes[] = $parseFunction();
}
return $nodes;
}
/**
* Returns a non-empty list of parse nodes, determined by
* the parseFn. This list begins with a lex token of openKind
* 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 $parseFunction
* @param string $closeKind
* @return array
* @throws SyntaxErrorException
*/
protected function many(string $openKind, callable $parseFunction, string $closeKind): array
{
$this->expect($openKind);
$nodes = [$parseFunction()];
while (!$this->skip($closeKind)) {
$nodes[] = $parseFunction();
}
return $nodes;
}
}
================================================
FILE: src/Language/ParserInterface.php
================================================
body = $body;
$this->name = $name;
$this->setLocationOffset($locationOffset);
}
/**
* @return int
*/
public function getBodyLength(): int
{
return mb_strlen($this->body);
}
/**
* @return string
*/
public function getBody(): string
{
return $this->body;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @return SourceLocation|null
*/
public function getLocationOffset(): ?SourceLocation
{
return $this->locationOffset;
}
/**
* @param string $body
* @return Source
*/
protected function setBody(string $body): Source
{
$this->body = $body;
return $this;
}
/**
* @param string $name
* @return Source
*/
protected function setName(string $name): Source
{
$this->name = $name;
return $this;
}
/**
* @param SourceLocation|null $locationOffset
* @return Source
* @throws InvariantException
*/
protected function setLocationOffset(?SourceLocation $locationOffset): Source
{
if (null !== $locationOffset) {
if ($locationOffset->getLine() < 1) {
throw new InvariantException("'line is 1-indexed and must be positive");
}
if ($locationOffset->getColumn() < 1) {
throw new InvariantException("'column is 1-indexed and must be positive'");
}
}
$this->locationOffset = $locationOffset ?? new SourceLocation();
return $this;
}
}
================================================
FILE: src/Language/SourceBuilderInterface.php
================================================
line = $line;
$this->column = $column;
}
/**
* @return int
*/
public function getLine(): int
{
return $this->line;
}
/**
* @return int
*/
public function getColumn(): int
{
return $this->column;
}
/**
* @param Source $source
* @param int $position
* @return SourceLocation
*/
public static function fromSource(Source $source, int $position): self
{
$line = 1;
$column = $position + 1;
$matches = [];
\preg_match_all("/\r\n|[\n\r]/", \substr($source->getBody(), 0, $position), $matches, PREG_OFFSET_CAPTURE);
foreach ($matches[0] as $index => $match) {
$line += 1;
$column = $position + 1 - ($match[1] + \strlen($match[0]));
}
return new static($line, $column);
}
/**
* @inheritdoc
*/
public function toArray(): array
{
return [
'line' => $this->line,
'column' => $this->column,
];
}
}
================================================
FILE: src/Language/StringSourceBuilder.php
================================================
body = $body;
}
/**
* @inheritdoc
*/
public function build(): Source
{
return new Source($this->body);
}
}
================================================
FILE: src/Language/SyntaxErrorException.php
================================================
kind = $kind;
$this->start = $start;
$this->end = $end;
$this->line = $line;
$this->column = $column;
$this->prev = $prev;
$this->value = $value;
}
/**
* @return string
*/
public function getKind(): string
{
return $this->kind;
}
/**
* @return int
*/
public function getStart(): int
{
return $this->start;
}
/**
* @return int
*/
public function getEnd(): int
{
return $this->end;
}
/**
* @return int
*/
public function getLine(): int
{
return $this->line;
}
/**
* @return int
*/
public function getColumn(): int
{
return $this->column;
}
/**
* @return Token|null
*/
public function getPrev(): ?Token
{
return $this->prev;
}
/**
* @return Token|null
*/
public function getNext(): ?Token
{
return $this->next;
}
/**
* @return string|null
*/
public function getValue(): ?string
{
return $this->value;
}
/**
* @param Token|null $next
* @return $this
*/
public function setNext(?Token $next)
{
$this->next = $next;
return $this;
}
/**
* @inheritdoc
*/
public function toArray(): array
{
return [
'kind' => $this->kind,
'line' => $this->line,
'column' => $this->column,
'value' => $this->value,
];
}
/**
* @return string
*/
public function __toString(): string
{
return $this->value !== null
? sprintf('%s "%s"', $this->kind, $this->value)
: $this->kind;
}
}
================================================
FILE: src/Language/TokenKindEnum.php
================================================
';
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';
/**
* @return array
* @throws \ReflectionException
*/
public static function values(): array
{
return array_values((new \ReflectionClass(__CLASS__))->getConstants());
}
}
================================================
FILE: src/Language/Visitor/ParallelVisitor.php
================================================
visitors = $visitors;
}
/**
* @inheritdoc
*/
public function enterNode(NodeInterface $node): VisitorResult
{
foreach ($this->visitors as $i => $visitor) {
if (!isset($this->skipping[$i])) {
$VisitorResult = $visitor->enterNode($node);
if ($VisitorResult->getAction() === VisitorResult::ACTION_BREAK) {
$this->skipping[$i] = true;
continue;
}
if (null === $VisitorResult->getValue()) {
$this->skipping[$i] = $node;
$VisitorResult = new VisitorResult($node);
}
}
}
return $VisitorResult ?? new VisitorResult($node);
}
/**
* @inheritdoc
*/
public function leaveNode(NodeInterface $node): VisitorResult
{
foreach ($this->visitors as $i => $visitor) {
if (!isset($this->skipping[$i])) {
$VisitorResult = $visitor->leaveNode($node);
if ($VisitorResult->getAction() === VisitorResult::ACTION_BREAK) {
$this->skipping[$i] = true;
continue;
}
} elseif ($this->skipping[$i] === $node) {
unset($this->skipping[$i]);
}
}
return $VisitorResult ?? new VisitorResult(null);
}
}
================================================
FILE: src/Language/Visitor/SpecificKindVisitor.php
================================================
getKind();
return $this->{$enterMethod}($node);
}
/**
* @inheritdoc
*/
public function leaveNode(NodeInterface $node): VisitorResult
{
$leaveMethod = 'leave' . $node->getKind();
return $this->{$leaveMethod}($node);
}
/**
* @param ArgumentNode $node
* @return VisitorResult
*/
protected function enterArgument(ArgumentNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param ArgumentNode $node
* @return VisitorResult
*/
protected function leaveArgument(ArgumentNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param BooleanValueNode $node
* @return VisitorResult
*/
protected function enterBooleanValue(BooleanValueNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param BooleanValueNode $node
* @return VisitorResult
*/
protected function leaveBooleanValue(BooleanValueNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param DirectiveDefinitionNode $node
* @return VisitorResult
*/
protected function enterDirectiveDefinition(DirectiveDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param DirectiveDefinitionNode $node
* @return VisitorResult
*/
protected function leaveDirectiveDefinition(DirectiveDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param DirectiveNode $node
* @return VisitorResult
*/
protected function enterDirective(DirectiveNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param DirectiveNode $node
* @return VisitorResult
*/
protected function leaveDirective(DirectiveNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param DocumentNode $node
* @return VisitorResult
*/
protected function enterDocument(DocumentNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param DocumentNode $node
* @return VisitorResult
*/
protected function leaveDocument(DocumentNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param EnumTypeDefinitionNode $node
* @return VisitorResult
*/
protected function enterEnumTypeDefinition(EnumTypeDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param EnumTypeDefinitionNode $node
* @return VisitorResult
*/
protected function leaveEnumTypeDefinition(EnumTypeDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param EnumTypeExtensionNode $node
* @return VisitorResult
*/
protected function enterEnumTypeExtension(EnumTypeExtensionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param EnumTypeExtensionNode $node
* @return VisitorResult
*/
protected function leaveEnumTypeExtension(EnumTypeExtensionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param EnumValueDefinitionNode $node
* @return VisitorResult
*/
protected function enterEnumValueDefinition(EnumValueDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param EnumValueDefinitionNode $node
* @return VisitorResult
*/
protected function leaveEnumValueDefinition(EnumValueDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param EnumValueNode $node
* @return VisitorResult
*/
protected function enterEnumValue(EnumValueNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param EnumValueNode $node
* @return VisitorResult
*/
protected function leaveEnumValue(EnumValueNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param FieldDefinitionNode $node
* @return VisitorResult
*/
protected function enterFieldDefinition(FieldDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param FieldDefinitionNode $node
* @return VisitorResult
*/
protected function leaveFieldDefinition(FieldDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param FieldNode $node
* @return VisitorResult
*/
protected function enterField(FieldNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param FieldNode $node
* @return VisitorResult
*/
protected function leaveField(FieldNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param FloatValueNode $node
* @return VisitorResult
*/
protected function enterFloatValue(FloatValueNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param FloatValueNode $node
* @return VisitorResult
*/
protected function leaveFloatValue(FloatValueNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param FragmentDefinitionNode $node
* @return VisitorResult
*/
protected function enterFragmentDefinition(FragmentDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param FragmentDefinitionNode $node
* @return VisitorResult
*/
protected function leaveFragmentDefinition(FragmentDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param FragmentSpreadNode $node
* @return VisitorResult
*/
protected function enterFragmentSpread(FragmentSpreadNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param FragmentSpreadNode $node
* @return VisitorResult
*/
protected function leaveFragmentSpread(FragmentSpreadNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param InlineFragmentNode $node
* @return VisitorResult
*/
protected function enterInlineFragment(InlineFragmentNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param InlineFragmentNode $node
* @return VisitorResult
*/
protected function leaveInlineFragment(InlineFragmentNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param InputObjectTypeDefinitionNode $node
* @return VisitorResult
*/
protected function enterInputObjectTypeDefinition(InputObjectTypeDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param InputObjectTypeDefinitionNode $node
* @return VisitorResult
*/
protected function leaveInputObjectTypeDefinition(InputObjectTypeDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param InputObjectTypeExtensionNode $node
* @return VisitorResult
*/
protected function enterInputObjectTypeExtension(InputObjectTypeExtensionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param InputObjectTypeExtensionNode $node
* @return VisitorResult
*/
protected function leaveInputObjectTypeExtension(InputObjectTypeExtensionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param InputValueDefinitionNode $node
* @return VisitorResult
*/
protected function enterInputValueDefinition(InputValueDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param InputValueDefinitionNode $node
* @return VisitorResult
*/
protected function leaveInputValueDefinition(InputValueDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param IntValueNode $node
* @return VisitorResult
*/
protected function enterIntValue(IntValueNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param IntValueNode $node
* @return VisitorResult
*/
protected function leaveIntValue(IntValueNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param InterfaceTypeDefinitionNode $node
* @return VisitorResult
*/
protected function enterInterfaceTypeDefinition(InterfaceTypeDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param InterfaceTypeDefinitionNode $node
* @return VisitorResult
*/
protected function leaveInterfaceTypeDefinition(InterfaceTypeDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param InterfaceTypeExtensionNode $node
* @return VisitorResult
*/
protected function enterInterfaceTypeExtension(InterfaceTypeExtensionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param InterfaceTypeExtensionNode $node
* @return VisitorResult
*/
protected function leaveInterfaceTypeExtension(InterfaceTypeExtensionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param ListTypeNode $node
* @return VisitorResult
*/
protected function enterListType(ListTypeNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param ListTypeNode $node
* @return VisitorResult
*/
protected function leaveListType(ListTypeNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param ListValueNode $node
* @return VisitorResult
*/
protected function enterListValue(ListValueNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param ListValueNode $node
* @return VisitorResult
*/
protected function leaveListValue(ListValueNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param NamedTypeNode $node
* @return VisitorResult
*/
protected function enterNamedType(NamedTypeNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param NamedTypeNode $node
* @return VisitorResult
*/
protected function leaveNamedType(NamedTypeNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param NameNode $node
* @return VisitorResult
*/
protected function enterName(NameNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param NameNode $node
* @return VisitorResult
*/
protected function leaveName(NameNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param NonNullTypeNode $node
* @return VisitorResult
*/
protected function enterNonNullType(NonNullTypeNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param NonNullTypeNode $node
* @return VisitorResult
*/
protected function leaveNonNullType(NonNullTypeNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param NullValueNode $node
* @return VisitorResult
*/
protected function enterNullValue(NullValueNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param NullValueNode $node
* @return VisitorResult
*/
protected function leaveNullValue(NullValueNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param ObjectFieldNode $node
* @return VisitorResult
*/
protected function enterObjectField(ObjectFieldNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param ObjectFieldNode $node
* @return VisitorResult
*/
protected function leaveObjectField(ObjectFieldNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param ObjectTypeDefinitionNode $node
* @return VisitorResult
*/
protected function enterObjectTypeDefinition(ObjectTypeDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param ObjectTypeDefinitionNode $node
* @return VisitorResult
*/
protected function leaveObjectTypeDefinition(ObjectTypeDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param ObjectTypeExtensionNode $node
* @return VisitorResult
*/
protected function enterObjectTypeExtension(ObjectTypeExtensionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param ObjectTypeExtensionNode $node
* @return VisitorResult
*/
protected function leaveObjectTypeExtension(ObjectTypeExtensionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param ObjectValueNode $node
* @return VisitorResult
*/
protected function enterObjectValue(ObjectValueNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param ObjectValueNode $node
* @return VisitorResult
*/
protected function leaveObjectValue(ObjectValueNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param OperationDefinitionNode $node
* @return VisitorResult
*/
protected function enterOperationDefinition(OperationDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param OperationDefinitionNode $node
* @return VisitorResult
*/
protected function leaveOperationDefinition(OperationDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param OperationTypeDefinitionNode $node
* @return VisitorResult
*/
protected function enterOperationTypeDefinition(OperationTypeDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param OperationTypeDefinitionNode $node
* @return VisitorResult
*/
protected function leaveOperationTypeDefinition(OperationTypeDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param ScalarTypeDefinitionNode $node
* @return VisitorResult
*/
protected function enterScalarTypeDefinition(ScalarTypeDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param ScalarTypeDefinitionNode $node
* @return VisitorResult
*/
protected function leaveScalarTypeDefinition(ScalarTypeDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param ScalarTypeExtensionNode $node
* @return VisitorResult
*/
protected function enterScalarTypeExtension(ScalarTypeExtensionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param ScalarTypeExtensionNode $node
* @return VisitorResult
*/
protected function leaveScalarTypeExtension(ScalarTypeExtensionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param SchemaDefinitionNode $node
* @return VisitorResult
*/
protected function enterSchemaDefinition(SchemaDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param SchemaDefinitionNode $node
* @return VisitorResult
*/
protected function leaveSchemaDefinition(SchemaDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param SchemaExtensionNode $node
* @return VisitorResult
*/
protected function enterSchemaExtension(SchemaExtensionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param SchemaExtensionNode $node
* @return VisitorResult
*/
protected function leaveSchemaExtension(SchemaExtensionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param SelectionSetNode $node
* @return VisitorResult
*/
protected function enterSelectionSet(SelectionSetNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param SelectionSetNode $node
* @return VisitorResult
*/
protected function leaveSelectionSet(SelectionSetNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param StringValueNode $node
* @return VisitorResult
*/
protected function enterStringValue(StringValueNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param StringValueNode $node
* @return VisitorResult
*/
protected function leaveStringValue(StringValueNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param UnionTypeDefinitionNode $node
* @return VisitorResult
*/
protected function enterUnionTypeDefinition(UnionTypeDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param UnionTypeDefinitionNode $node
* @return VisitorResult
*/
protected function leaveUnionTypeDefinition(UnionTypeDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param UnionTypeExtensionNode $node
* @return VisitorResult
*/
protected function enterUnionTypeExtension(UnionTypeExtensionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param UnionTypeExtensionNode $node
* @return VisitorResult
*/
protected function leaveUnionTypeExtension(UnionTypeExtensionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param VariableDefinitionNode $node
* @return VisitorResult
*/
protected function enterVariableDefinition(VariableDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param VariableDefinitionNode $node
* @return VisitorResult
*/
protected function leaveVariableDefinition(VariableDefinitionNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param VariableNode $node
* @return VisitorResult
*/
protected function enterVariable(VariableNode $node): VisitorResult
{
return new VisitorResult($node);
}
/**
* @param VariableNode $node
* @return VisitorResult
*/
protected function leaveVariable(VariableNode $node): VisitorResult
{
return new VisitorResult($node);
}
}
================================================
FILE: src/Language/Visitor/TypeInfoVisitor.php
================================================
typeInfo = $typeInfo;
$this->visitor = $visitor;
}
/**
* @inheritdoc
*
* @throws ConversionException
* @throws InvariantException
*/
public function enterNode(NodeInterface $node): VisitorResult
{
$schema = $this->typeInfo->getSchema();
if ($node instanceof SelectionSetNode) {
$namedType = getNamedType($this->typeInfo->getType());
$this->typeInfo->pushParentType($namedType instanceof CompositeTypeInterface ? $namedType : null);
} elseif ($node instanceof FieldNode) {
$parentType = $this->typeInfo->getParentType();
$fieldDefinition = null;
$fieldType = null;
if (null !== $parentType) {
$fieldDefinition = $this->typeInfo->resolveFieldDefinition($schema, $parentType, $node);
if (null !== $fieldDefinition) {
$fieldType = $fieldDefinition->getType();
}
}
$this->typeInfo->pushFieldDefinition($fieldDefinition);
$this->typeInfo->pushType(isOutputType($fieldType) ? $fieldType : null);
} elseif ($node instanceof DirectiveNode) {
$this->typeInfo->setDirective($schema->getDirective($node->getNameValue()));
} elseif ($node instanceof OperationDefinitionNode) {
$type = null;
$operation = $node->getOperation();
if ($operation === 'query') {
$type = $schema->getQueryType();
} elseif ($operation === 'mutation') {
$type = $schema->getMutationType();
} elseif ($operation === 'subscription') {
$type = $schema->getSubscriptionType();
}
$this->typeInfo->pushType($type instanceof ObjectType ? $type : null);
} elseif ($node instanceof InlineFragmentNode || $node instanceof FragmentDefinitionNode) {
$typeCondition = $node->getTypeCondition();
$outputType = null !== $typeCondition
? TypeASTConverter::convert($schema, $typeCondition)
: getNamedType($this->typeInfo->getType());
$this->typeInfo->pushType(isOutputType($outputType) ? $outputType : null);
} elseif ($node instanceof VariableDefinitionNode) {
$inputType = TypeASTConverter::convert($schema, $node->getType());
/** @noinspection PhpParamsInspection */
$this->typeInfo->pushInputType(isInputType($inputType) ? $inputType : null);
} elseif ($node instanceof ArgumentNode) {
$argumentType = null;
/** @var Argument|null $argumentDefinition */
$argumentDefinition = null;
$fieldOrDirective = $this->typeInfo->getDirective() ?: $this->typeInfo->getFieldDefinition();
if (null !== $fieldOrDirective) {
$argumentDefinition = find(
$fieldOrDirective->getArguments(),
function (Argument $argument) use ($node) {
return $argument->getName() === $node->getNameValue();
}
);
if (null !== $argumentDefinition) {
$argumentType = $argumentDefinition->getType();
}
}
$this->typeInfo->setArgument($argumentDefinition);
$this->typeInfo->pushDefaultValue(null !== $argumentDefinition
? $argumentDefinition->getDefaultValue()
: null);
$this->typeInfo->pushInputType(isInputType($argumentType) ? $argumentType : null);
} elseif ($node instanceof ListValueNode) {
$listType = getNullableType($this->typeInfo->getInputType());
$itemType = $listType instanceof ListType ? $listType->getOfType() : $listType;
// List positions never have a default value.
$this->typeInfo->pushDefaultValue(null);
$this->typeInfo->pushInputType(isInputType($itemType) ? $itemType : null);
} elseif ($node instanceof ObjectFieldNode) {
$objectType = getNamedType($this->typeInfo->getInputType());
$inputField = null;
$inputFieldType = null;
if ($objectType instanceof InputObjectType) {
$fields = $objectType->getFields();
$inputField = $fields[$node->getNameValue()] ?? null;
if (null !== $inputField) {
$inputFieldType = $inputField->getType();
}
}
$this->typeInfo->pushDefaultValue(null !== $inputField ? $inputField->getDefaultValue() : null);
/** @noinspection PhpParamsInspection */
$this->typeInfo->pushInputType(isInputType($inputFieldType) ? $inputFieldType : null);
} elseif ($node instanceof EnumValueNode) {
$enumType = getNamedType($this->typeInfo->getInputType());
$enumValue = null;
if ($enumType instanceof EnumType) {
$enumValue = $enumType->getValue($node->getValue());
}
$this->typeInfo->setEnumValue($enumValue);
}
return $this->visitor->enterNode($node);
}
/**
* @inheritdoc
*/
public function leaveNode(NodeInterface $node): VisitorResult
{
$VisitorResult = $this->visitor->leaveNode($node);
$newNode = $VisitorResult->getValue();
if ($newNode instanceof SelectionSetNode) {
$this->typeInfo->popParentType();
} elseif ($newNode instanceof FieldNode) {
$this->typeInfo->popFieldDefinition();
$this->typeInfo->popType();
} elseif ($newNode instanceof DirectiveNode) {
$this->typeInfo->setDirective(null);
} elseif ($newNode instanceof OperationDefinitionNode) {
$this->typeInfo->popType();
} elseif ($newNode instanceof InlineFragmentNode || $newNode instanceof FragmentDefinitionNode) {
$this->typeInfo->popType();
} elseif ($newNode instanceof VariableDefinitionNode) {
$this->typeInfo->popInputType();
} elseif ($newNode instanceof ArgumentNode) {
$this->typeInfo->setArgument(null);
$this->typeInfo->popDefaultValue();
$this->typeInfo->popInputType();
} elseif ($newNode instanceof ListValueNode) {
$this->typeInfo->popInputType();
} elseif ($newNode instanceof ObjectFieldNode) {
$this->typeInfo->popDefaultValue();
$this->typeInfo->popInputType();
} elseif ($newNode instanceof EnumValueNode) {
$this->typeInfo->setEnumValue(null);
}
return $VisitorResult;
}
}
================================================
FILE: src/Language/Visitor/Visitor.php
================================================
enterCallback = $enterCallback;
$this->leaveCallback = $leaveCallback;
}
/**
* @inheritdoc
*/
public function enterNode(NodeInterface $node): VisitorResult
{
return null !== $this->enterCallback
? \call_user_func($this->enterCallback, $node)
: new VisitorResult($node);
}
/**
* @inheritdoc
*/
public function leaveNode(NodeInterface $node): VisitorResult
{
return null !== $this->leaveCallback
? \call_user_func($this->leaveCallback, $node)
: new VisitorResult($node);
}
}
================================================
FILE: src/Language/Visitor/VisitorBreak.php
================================================
visitor = $visitor;
$this->key = $key;
$this->parent = $parent;
$this->path = $path;
$this->ancestors = $ancestors;
}
/**
* Appends a key to the path.
* @param string $key
*/
public function addOneToPath(string $key)
{
$this->path[] = $key;
}
/**
* Removes the last item from the path.
*/
public function removeOneFromPath()
{
$this->path = \array_slice($this->path, 0, -1);
}
/**
* Adds an ancestor.
* @param NodeInterface $node
*/
public function addAncestor(NodeInterface $node)
{
$this->ancestors[] = $node;
}
/**
* Removes the last ancestor.
*/
public function removeAncestor()
{
$this->ancestors = \array_slice($this->ancestors, 0, -1);
}
/**
* @inheritdoc
*/
public function getAncestor(int $depth = 1): ?NodeInterface
{
if (empty($this->ancestors)) {
return null;
}
$index = \count($this->ancestors) - $depth;
return $this->ancestors[$index] ?? null;
}
/**
* @return VisitorInterface
*/
public function getVisitor(): VisitorInterface
{
return $this->visitor;
}
/**
* @return int|null|string
*/
public function getKey()
{
return $this->key;
}
/**
* @return NodeInterface|null
*/
public function getParent(): ?NodeInterface
{
return $this->parent;
}
/**
* @return array
*/
public function getPath(): array
{
return $this->path;
}
/**
* @return array
*/
public function getAncestors(): array
{
return $this->ancestors;
}
}
================================================
FILE: src/Language/Visitor/VisitorInterface.php
================================================
value = $value;
$this->action = $action;
}
/**
* @return NodeInterface|null
*/
public function getValue(): ?NodeInterface
{
return $this->value;
}
/**
* @return string
*/
public function getAction(): string
{
return $this->action;
}
/**
* @param string $action
* @return self
*/
public function setAction(string $action): self
{
$this->action = $action;
return $this;
}
}
================================================
FILE: src/Language/blockStringValue.php
================================================
0) {
for ($i = 1; $i < $lineCount; $i++) {
$lines[$i] = sliceString($lines[$i], $commonIndent);
}
}
while (\count($lines) > 0 && isBlank($lines[0])) {
\array_shift($lines);
}
while (($lineCount = \count($lines)) > 0 && isBlank($lines[$lineCount - 1])) {
\array_pop($lines);
}
return \implode("\n", $lines);
}
/**
* @param string $string
* @return int
*/
function leadingWhitespace(string $string): int
{
$i = 0;
$length = \mb_strlen($string);
while ($i < $length && ($string[$i] === ' ' || $string[$i] === "\t")) {
$i++;
}
return $i;
}
/**
* @param string $string
* @return bool
*/
function isBlank(string $string): bool
{
return leadingWhitespace($string) === \mb_strlen($string);
}
================================================
FILE: src/Language/utils.php
================================================
';
}
return $code < 0x007F
// Trust JSON for ASCII.
? \json_encode(\mb_chr($code, 'UTF-8'))
// Otherwise print the escaped form.
: '"\\u' . \dechex($code) . '"';
}
/**
* @param string $string
* @param int $start
* @param int|null $end
* @return string
*/
function sliceString(string $string, int $start, int $end = null): string
{
$length = $end !== null ? $end - $start : \mb_strlen($string) - $start;
return \mb_substr($string, $start, $length);
}
/**
* @param int $code
* @return bool
*/
function isLetter(int $code): bool
{
return ($code >= 65 && $code <= 90) || ($code >= 97 && $code <= 122); // a-z or A-Z
}
/**
* @param int $code
* @return bool
*/
function isNumber(int $code): bool
{
return $code >= 48 && $code <= 57; // 0-9
}
/**
* @param int $code
* @return bool
*/
function isUnderscore(int $code): bool
{
return $code === 95; // _
}
/**
* @param int $code
* @return bool
*/
function isAlphaNumeric(int $code): bool
{
return isLetter($code) || isNumber($code) || isUnderscore($code);
}
/**
* @param int $code
* @return bool
*/
function isLineTerminator(int $code): bool
{
return $code === 0x000a || $code === 0x000d;
}
/**
* @param int $code
* @return bool
*/
function isSourceCharacter(int $code): bool
{
return $code < 0x0020 && $code !== 0x0009; // any source character EXCEPT HT (Horizontal Tab)
}
/**
* @param string $value
* @return bool
*/
function isOperation(string $value): bool
{
return $value === 'query' || $value === 'mutation' || $value === 'subscription';
}
/**
* @param array $location
* @return array|null
*/
function locationShorthandToArray(array $location): ?array
{
return isset($location[0], $location[1]) ? ['line' => $location[0], 'column' => $location[1]] : null;
}
/**
* @param array $locations
* @return array
*/
function locationsShorthandToArray(array $locations): array
{
return array_map(function ($shorthand) {
return locationShorthandToArray($shorthand);
}, $locations);
}
/**
* @param array $array
* @return string
*/
function block(array $array): string
{
return !empty($array) ? "{\n" . indent(implode("\n", $array)) . "\n}" : '';
}
/**
* @param string $start
* @param null|string $maybeString
* @param null|string $end
* @return string
*/
function wrap(string $start, ?string $maybeString = null, ?string $end = null): string
{
return null !== $maybeString ? ($start . $maybeString . ($end ?? '')) : '';
}
/**
* @param null|string $maybeString
* @return string
*/
function indent(?string $maybeString): string
{
return null !== $maybeString ? ' ' . preg_replace("/\n/", "\n ", $maybeString) : '';
}
/**
* @param string $str
* @return string
*/
function dedent(string $str): string
{
$trimmed = \preg_replace("/^\n*|[ \t]*$/", '', $str); // Remove leading newline and trailing whitespace
$matches = [];
\preg_match("/^[ \t]*/", $trimmed, $matches); // Figure out indent
$indent = $matches[0];
return \str_replace($indent, '', $trimmed); // Remove indent
}
/**
* @param string $value
* @param string $indentation
* @param bool $preferMultipleLines
* @return string
*/
function printBlockString(string $value, string $indentation = '', bool $preferMultipleLines = false): string {
$isSingleLine = false === \strpos($value, "\n");
$hasLeadingSpace = $value[0] === ' ' || $value[0] === "\t";
$hasTrailingQuote = $value[\strlen($value) - 1] === '"';
$printAsMultipleLines = !$isSingleLine || $hasTrailingQuote || $preferMultipleLines;
$result = '';
// Format a multi-line block quote to account for leading space.
if ($printAsMultipleLines && !($isSingleLine && $hasLeadingSpace)) {
$result .= "\n" . $indentation;
}
$result .= \strlen($indentation) > 0
? \str_replace("\n", "\n" . $indentation, $value)
: $value;
if ($printAsMultipleLines) {
$result .= "\n";
}
return '"""' . escapeQuotes($result) . '"""';
}
================================================
FILE: src/Schema/Building/BuildInfo.php
================================================
document = $document;
$this->typeDefinitionMap = $typeDefinitionMap;
$this->directiveDefinitions = $directiveDefinitions;
$this->operationTypeDefinitions = $operationTypeDefinitions;
$this->schemaDefinition = $schemaDefinition;
}
/**
* @param string $typeName
* @return TypeNodeInterface|null
*/
public function getTypeDefinition(string $typeName): ?TypeNodeInterface
{
return $this->typeDefinitionMap[$typeName] ?? null;
}
/**
* @param string $operation
* @return NodeInterface|null
*/
public function getOperationTypeDefinition(string $operation): ?NodeInterface
{
// If we have a schema definition, see if it defines a type, if not we should return null.
// Otherwise, see if we there is a suitable type available in the type definition map.
return null !== $this->schemaDefinition
? $this->operationTypeDefinitions[$operation] ?? null
: $this->typeDefinitionMap[\ucfirst($operation)] ?? null;
}
/**
* @return DocumentNode
*/
public function getDocument(): DocumentNode
{
return $this->document;
}
/**
* @return SchemaDefinitionNode|null
*/
public function getSchemaDefinition(): ?SchemaDefinitionNode
{
return $this->schemaDefinition;
}
/**
* @return TypeNodeInterface[]
*/
public function getTypeDefinitionMap(): array
{
return $this->typeDefinitionMap;
}
/**
* @return DirectiveDefinitionNode[]
*/
public function getDirectiveDefinitions(): array
{
return $this->directiveDefinitions;
}
}
================================================
FILE: src/Schema/Building/BuildingContext.php
================================================
resolverRegistry = $resolverRegistry;
$this->definitionBuilder = $definitionBuilder;
$this->info = $info;
}
/**
* @return TypeInterface|null
*/
public function buildQueryType(): ?TypeInterface
{
$definition = $this->info->getOperationTypeDefinition('query');
return null !== $definition ? $this->definitionBuilder->buildType($definition) : null;
}
/**
* @return TypeInterface|null
*/
public function buildMutationType(): ?TypeInterface
{
$definition = $this->info->getOperationTypeDefinition('mutation');
return null !== $definition ? $this->definitionBuilder->buildType($definition) : null;
}
/**
* @return TypeInterface|null
*/
public function buildSubscriptionType(): ?TypeInterface
{
$definition = $this->info->getOperationTypeDefinition('subscription');
return null !== $definition ? $this->definitionBuilder->buildType($definition) : null;
}
/**
* @return TypeInterface[]
*/
public function buildTypes(): array
{
return \array_map(function (NamedTypeNodeInterface $definition) {
return $this->definitionBuilder->buildType($definition);
}, \array_values($this->info->getTypeDefinitionMap()));
}
/**
* @return Directive[]
*/
public function buildDirectives(): array
{
$directives = \array_map(function (DirectiveDefinitionNode $definition) {
return $this->definitionBuilder->buildDirective($definition);
}, $this->info->getDirectiveDefinitions());
$specifiedDirectivesMap = [
'skip' => SkipDirective(),
'include' => IncludeDirective(),
'deprecated' => DeprecatedDirective(),
];
foreach ($specifiedDirectivesMap as $name => $directive) {
if (!arraySome($directives, function (Directive $directive) use ($name) {
return $directive->getName() === $name;
})) {
$directives[] = $directive;
}
}
return $directives;
}
/**
* @return SchemaDefinitionNode|null
*/
public function getSchemaDefinition(): ?SchemaDefinitionNode
{
return $this->info->getSchemaDefinition();
}
}
================================================
FILE: src/Schema/Building/BuildingContextInterface.php
================================================
createContext($document, $resolverRegistry, $options);
/** @noinspection PhpUnhandledExceptionInspection */
return newSchema([
'query' => $context->buildQueryType(),
'mutation' => $context->buildMutationType(),
'subscription' => $context->buildSubscriptionType(),
'types' => $context->buildTypes(),
'directives' => $context->buildDirectives(),
'astNode' => $context->getSchemaDefinition(),
'assumeValid' => $options['assumeValid'] ?? false,
]);
}
/**
* @param DocumentNode $document
* @param ResolverRegistryInterface $resolverRegistry
* @param array $options
* @return BuildingContextInterface
* @throws SchemaBuildingException
*/
protected function createContext(
DocumentNode $document,
ResolverRegistryInterface $resolverRegistry,
array $options
): BuildingContextInterface {
$info = $this->createInfo($document);
$definitionBuilder = new DefinitionBuilder(
$info->getTypeDefinitionMap(),
$resolverRegistry,
$options['types'] ?? [],
$options['directives'] ?? [],
null // use the default resolveType-function
);
return new BuildingContext($resolverRegistry, $definitionBuilder, $info);
}
/**
* @param DocumentNode $document
* @return BuildInfo
* @throws SchemaBuildingException
*/
protected function createInfo(DocumentNode $document): BuildInfo
{
$schemaDefinition = null;
$typeDefinitionMap = [];
$directiveDefinitions = [];
foreach ($document->getDefinitions() as $definition) {
if ($definition instanceof SchemaDefinitionNode) {
if (null !== $schemaDefinition) {
throw new SchemaBuildingException('Must provide only one schema definition.');
}
$schemaDefinition = $definition;
continue;
}
if ($definition instanceof DirectiveDefinitionNode) {
$directiveDefinitions[] = $definition;
continue;
}
if ($definition instanceof TypeSystemDefinitionNodeInterface && $definition instanceof NameAwareInterface) {
$typeName = $definition->getNameValue();
if (isset($typeDefinitionMap[$typeName])) {
throw new SchemaBuildingException(\sprintf('Type "%s" was defined more than once.', $typeName));
}
$typeDefinitionMap[$typeName] = $definition;
continue;
}
}
return new BuildInfo(
$document,
$typeDefinitionMap,
$directiveDefinitions,
null !== $schemaDefinition ? $this->getOperationTypeDefinitions($schemaDefinition, $typeDefinitionMap) : [],
$schemaDefinition
);
}
/**
* @param SchemaDefinitionNode $node
* @return array
* @throws SchemaBuildingException
*/
protected function getOperationTypeDefinitions(SchemaDefinitionNode $node, array $typeDefinitionMap): array
{
$definitions = [];
foreach ($node->getOperationTypes() as $operationTypeDefinition) {
$operationType = $operationTypeDefinition->getType();
if (!$operationType instanceof NamedTypeNode) {
continue; // TODO: Throw exception?
}
$typeName = $operationType->getNameValue();
$operation = $operationTypeDefinition->getOperation();
if (isset($definitions[$typeName])) {
throw new SchemaBuildingException(
\sprintf('Must provide only one %s type in schema.', $operation)
);
}
if (!isset($typeDefinitionMap[$typeName])) {
throw new SchemaBuildingException(
\sprintf('Specified %s type %s not found in document.', $operation, $typeName)
);
}
$definitions[$operation] = $operationType;
}
return $definitions;
}
}
================================================
FILE: src/Schema/Building/SchemaBuilderInterface.php
================================================
container->add(SchemaBuilderInterface::class, SchemaBuilder::class);
}
}
================================================
FILE: src/Schema/DefinitionBuilder.php
================================================
typeDefinitionsMap = $typeDefinitionsMap;
$this->resolverRegistry = $resolverRegistry;
$this->resolveTypeFunction = $resolveTypeCallback ?? [$this, 'defaultTypeResolver'];
$this->registerTypes($types);
$this->registerDirectives($directives);
}
/**
* @inheritdoc
*/
public function buildTypes(array $nodes): array
{
return \array_map(function (NamedTypeNodeInterface $node) {
return $this->buildType($node);
}, $nodes);
}
/**
* @inheritdoc
*/
public function buildType(NamedTypeNodeInterface $node): NamedTypeInterface
{
$typeName = $node->getNameValue();
if (isset($this->types[$typeName])) {
return $this->types[$typeName];
}
if ($node instanceof NamedTypeNode) {
$definition = $this->getTypeDefinition($typeName);
/** @noinspection PhpUnhandledExceptionInspection */
$type = null !== $definition
? $this->buildNamedType($definition)
: $this->resolveType($node);
} else {
/** @noinspection PhpUnhandledExceptionInspection */
$type = $this->buildNamedType($node);
}
return $this->types[$typeName] = $type;
}
/**
* @inheritdoc
*/
public function buildDirective(DirectiveDefinitionNode $node): Directive
{
$directiveName = $node->getNameValue();
if (isset($this->directives[$directiveName])) {
return $this->directives[$directiveName];
}
/** @noinspection PhpUnhandledExceptionInspection */
$directive = newDirective([
'name' => $node->getNameValue(),
'description' => $node->getDescriptionValue(),
'locations' => \array_map(function (NameNode $node) {
return $node->getValue();
}, $node->getLocations()),
'args' => $node->hasArguments() ? $this->buildArguments($node->getArguments()) : [],
'astNode' => $node,
]);
return $this->directives[$directiveName] = $directive;
}
/**
* @inheritdoc
*/
public function buildField($node, ?callable $resolve = null): array
{
/** @noinspection PhpUnhandledExceptionInspection */
return [
'type' => $this->buildWrappedType($node->getType()),
'description' => $node->getDescriptionValue(),
'args' => $node->hasArguments() ? $this->buildArguments($node->getArguments()) : [],
'deprecationReason' => $this->getDeprecationReason($node),
'resolve' => $resolve,
'astNode' => $node,
];
}
/**
* @param TypeNodeInterface $typeNode
* @return TypeInterface
* @throws InvariantException
* @throws InvalidTypeException
*/
protected function buildWrappedType(TypeNodeInterface $typeNode): TypeInterface
{
/** @noinspection PhpUnhandledExceptionInspection */
$typeDefinition = $this->buildType($this->getNamedTypeNode($typeNode));
return $this->buildWrappedTypeRecursive($typeDefinition, $typeNode);
}
/**
* @param NamedTypeInterface $innerType
* @param TypeNodeInterface $inputTypeNode
* @return TypeInterface
* @throws InvariantException
* @throws InvalidTypeException
*/
protected function buildWrappedTypeRecursive(
NamedTypeInterface $innerType,
TypeNodeInterface $inputTypeNode
): TypeInterface {
if ($inputTypeNode instanceof ListTypeNode) {
return newList($this->buildWrappedTypeRecursive($innerType, $inputTypeNode->getType()));
}
if ($inputTypeNode instanceof NonNullTypeNode) {
return newNonNull($this->buildWrappedTypeRecursive($innerType, $inputTypeNode->getType()));
}
return $innerType;
}
/**
* @param array $customTypes
*/
protected function registerTypes(array $customTypes)
{
$typesMap = keyMap(
\array_merge($customTypes, specifiedScalarTypes(), introspectionTypes()),
function (NamedTypeInterface $type) {
return $type->getName();
}
);
foreach ($typesMap as $typeName => $type) {
$this->types[$typeName] = $type;
}
}
/**
* @param array $customDirectives
*/
protected function registerDirectives(array $customDirectives)
{
$directivesMap = keyMap(
\array_merge($customDirectives, specifiedDirectives()),
function (Directive $directive) {
return $directive->getName();
}
);
foreach ($directivesMap as $directiveName => $directive) {
$this->directives[$directiveName] = $directive;
}
}
/**
* @param array $nodes
* @return array
*/
protected function buildArguments(array $nodes): array
{
return keyValueMap(
$nodes,
function (InputValueDefinitionNode $value) {
return $value->getNameValue();
},
function (InputValueDefinitionNode $value): array {
$type = $this->buildWrappedType($value->getType());
$defaultValue = $value->getDefaultValue();
return [
'type' => $type,
'description' => $value->getDescriptionValue(),
'defaultValue' => null !== $defaultValue
? ValueASTConverter::convert($defaultValue, $type)
: null,
'astNode' => $value,
];
});
}
/**
* @param TypeNodeInterface $node
* @return NamedTypeInterface
* @throws LanguageException
*/
protected function buildNamedType(TypeNodeInterface $node): NamedTypeInterface
{
if ($node instanceof ObjectTypeDefinitionNode) {
return $this->buildObjectType($node);
}
if ($node instanceof InterfaceTypeDefinitionNode) {
return $this->buildInterfaceType($node);
}
if ($node instanceof EnumTypeDefinitionNode) {
return $this->buildEnumType($node);
}
if ($node instanceof UnionTypeDefinitionNode) {
return $this->buildUnionType($node);
}
if ($node instanceof ScalarTypeDefinitionNode) {
return $this->buildScalarType($node);
}
if ($node instanceof InputObjectTypeDefinitionNode) {
/** @noinspection PhpUnhandledExceptionInspection */
return $this->buildInputObjectType($node);
}
throw new LanguageException(\sprintf('Type kind "%s" not supported.', $node->getKind()));
}
/**
* @param ObjectTypeDefinitionNode $node
* @return ObjectType
*/
protected function buildObjectType(ObjectTypeDefinitionNode $node): ObjectType
{
/** @noinspection PhpUnhandledExceptionInspection */
return newObjectType([
'name' => $node->getNameValue(),
'description' => $node->getDescriptionValue(),
'fields' => $node->hasFields() ? function () use ($node) {
return $this->buildFields($node);
} : [],
// 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.
'interfaces' => function () use ($node) {
return $node->hasInterfaces() ? \array_map(function (NamedTypeNodeInterface $interface) {
return $this->buildType($interface);
}, $node->getInterfaces()) : [];
},
'astNode' => $node,
]);
}
/**
* @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|InputObjectTypeDefinitionNode $node
* @return array
*/
protected function buildFields($node): array
{
return keyValueMap(
$node->getFields(),
function ($value) {
/** @var FieldDefinitionNode|InputValueDefinitionNode $value */
return $value->getNameValue();
},
function ($value) use ($node) {
/** @var FieldDefinitionNode|InputValueDefinitionNode $value */
return $this->buildField($value,
$this->getFieldResolver($node->getNameValue(), $value->getNameValue()));
}
);
}
/**
* @param string $typeName
* @param string $fieldName
* @return callable|null
*/
protected function getFieldResolver(string $typeName, string $fieldName): ?callable
{
return null !== $this->resolverRegistry
? $this->resolverRegistry->getFieldResolver($typeName, $fieldName)
: null;
}
/**
* @param InterfaceTypeDefinitionNode $node
* @return InterfaceType
*/
protected function buildInterfaceType(InterfaceTypeDefinitionNode $node): InterfaceType
{
/** @noinspection PhpUnhandledExceptionInspection */
return newInterfaceType([
'name' => $node->getNameValue(),
'description' => $node->getDescriptionValue(),
'fields' => $node->hasFields() ? function () use ($node): array {
return $this->buildFields($node);
} : [],
'resolveType' => $this->getTypeResolver($node->getNameValue()),
'astNode' => $node,
]);
}
/**
* @param EnumTypeDefinitionNode $node
* @return EnumType
*/
protected function buildEnumType(EnumTypeDefinitionNode $node): EnumType
{
/** @noinspection PhpUnhandledExceptionInspection */
return newEnumType([
'name' => $node->getNameValue(),
'description' => $node->getDescriptionValue(),
'values' => $node->hasValues() ? keyValueMap(
$node->getValues(),
function (EnumValueDefinitionNode $value): ?string {
return $value->getNameValue();
},
function (EnumValueDefinitionNode $value): array {
return [
'description' => $value->getDescriptionValue(),
'deprecationReason' => $this->getDeprecationReason($value),
'astNode' => $value,
];
}
) : [],
'astNode' => $node,
]);
}
/**
* @param UnionTypeDefinitionNode $node
* @return UnionType
*/
protected function buildUnionType(UnionTypeDefinitionNode $node): UnionType
{
/** @noinspection PhpUnhandledExceptionInspection */
return newUnionType([
'name' => $node->getNameValue(),
'description' => $node->getDescriptionValue(),
'types' => $node->hasTypes() ? \array_map(function (NamedTypeNodeInterface $type) {
return $this->buildType($type);
}, $node->getTypes()) : [],
'resolveType' => $this->getTypeResolver($node->getNameValue()),
'astNode' => $node,
]);
}
/**
* @param string $typeName
* @return callable|null
*/
protected function getTypeResolver(string $typeName): ?callable
{
return null !== $this->resolverRegistry
? $this->resolverRegistry->getTypeResolver($typeName)
: null;
}
/**
* @param ScalarTypeDefinitionNode $node
* @return ScalarType
*/
protected function buildScalarType(ScalarTypeDefinitionNode $node): ScalarType
{
/** @noinspection PhpUnhandledExceptionInspection */
return newScalarType([
'name' => $node->getNameValue(),
'description' => $node->getDescriptionValue(),
'serialize' => function ($value) {
return $value;
},
'astNode' => $node,
]);
}
/**
* @param InputObjectTypeDefinitionNode $node
* @return InputObjectType
* @throws InvariantException
*/
protected function buildInputObjectType(InputObjectTypeDefinitionNode $node): InputObjectType
{
return newInputObjectType([
'name' => $node->getNameValue(),
'description' => $node->getDescriptionValue(),
'fields' => $node->hasFields() ? function () use ($node) {
return keyValueMap(
$node->getFields(),
function (InputValueDefinitionNode $value): ?string {
return $value->getNameValue();
},
function (InputValueDefinitionNode $value): array {
$type = $this->buildWrappedType($value->getType());
$defaultValue = $value->getDefaultValue();
return [
'type' => $type,
'description' => $value->getDescriptionValue(),
'defaultValue' => null !== $defaultValue
? ValueASTConverter::convert($defaultValue, $type)
: null,
'astNode' => $value,
];
}
);
} : [],
'astNode' => $node,
]);
}
/**
* @param NamedTypeNode $node
* @return NamedTypeInterface
*/
protected function resolveType(NamedTypeNode $node): NamedTypeInterface
{
return \call_user_func($this->resolveTypeFunction, $node);
}
/**
* @param NamedTypeNode $node
* @return NamedTypeInterface|null
*/
public function defaultTypeResolver(NamedTypeNode $node): ?NamedTypeInterface
{
return $this->types[$node->getNameValue()] ?? null;
}
/**
* @param string $typeName
* @return NamedTypeNodeInterface|null
*/
protected function getTypeDefinition(string $typeName): ?NamedTypeNodeInterface
{
return $this->typeDefinitionsMap[$typeName] ?? null;
}
/**
* @param TypeNodeInterface $typeNode
* @return NamedTypeNodeInterface
*/
protected function getNamedTypeNode(TypeNodeInterface $typeNode): NamedTypeNodeInterface
{
$namedType = $typeNode;
while ($namedType instanceof ListTypeNode || $namedType instanceof NonNullTypeNode) {
$namedType = $namedType->getType();
}
/** @var NamedTypeNodeInterface $namedType */
return $namedType;
}
/**
* @param NodeInterface|EnumValueDefinitionNode|FieldDefinitionNode $node
* @return null|string
* @throws InvariantException
* @throws ExecutionException
*/
protected function getDeprecationReason(NodeInterface $node): ?string
{
if (isset($this->directives['deprecated'])) {
$deprecated = ValuesResolver::coerceDirectiveValues($this->directives['deprecated'], $node);
return $deprecated['reason'] ?? null;
}
return null;
}
}
================================================
FILE: src/Schema/DefinitionBuilderInterface.php
================================================
options = $options;
return $this->printFilteredSchema(
$schema,
function (Directive $directive): bool {
return !isSpecifiedDirective($directive);
},
function (NamedTypeInterface $type): bool {
return !isSpecifiedScalarType($type) && !isIntrospectionType($type);
}
);
}
/**
* @inheritdoc
* @throws PrintException
* @throws InvariantException
*/
public function printIntrospectionSchema(Schema $schema, array $options = []): string
{
$this->options = $options;
return $this->printFilteredSchema(
$schema,
function (Directive $directive): bool {
return isSpecifiedDirective($directive);
},
function (NamedTypeInterface $type): bool {
return isIntrospectionType($type);
}
);
}
/**
* @param DefinitionInterface $definition
* @return string
* @throws PrintException
* @throws InvariantException
*/
public function print(DefinitionInterface $definition): string
{
if ($definition instanceof Schema) {
return $this->printSchemaDefinition($definition);
}
if ($definition instanceof Directive) {
return $this->printDirectiveDefinition($definition);
}
if ($definition instanceof NamedTypeInterface) {
return $this->printType($definition);
}
throw new PrintException(\sprintf('Invalid definition object: %s.', toString($definition)));
}
/**
* @param Schema $schema
* @param callable $directiveFilter
* @param callable $typeFilter
* @return string
* @throws PrintException
* @throws InvariantException
*/
protected function printFilteredSchema(
Schema $schema,
callable $directiveFilter,
callable $typeFilter
): string {
/** @noinspection PhpParamsInspection */
$lines = \array_filter(\array_merge(
[$this->printOne($schema)],
$this->printMany($this->getSchemaDirectives($schema, $directiveFilter)),
$this->printMany($this->getSchemaTypes($schema, $typeFilter))
));
return printArray("\n\n", $lines) . "\n";
}
/**
* @param Schema $schema
* @param callable $filter
* @return array
*/
protected function getSchemaDirectives(Schema $schema, callable $filter): array
{
return \array_filter($schema->getDirectives(), $filter);
}
/**
* @param Schema $schema
* @param callable $filter
* @return array
*/
protected function getSchemaTypes(Schema $schema, callable $filter): array
{
$types = \array_filter(\array_values($schema->getTypeMap()), $filter);
\usort($types, function (NamedTypeInterface $typeA, NamedTypeInterface $typeB) {
return \strcasecmp($typeA->getName(), $typeB->getName());
});
return $types;
}
/**
* @param Schema $definition
* @return string
*/
protected function printSchemaDefinition(Schema $definition): string
{
if ($this->isSchemaOfCommonNames($definition)) {
return '';
}
$operationTypes = [];
if (null !== ($queryType = $definition->getQueryType())) {
$operationTypes[] = " query: {$queryType->getName()}";
}
if (null !== ($mutationType = $definition->getMutationType())) {
$operationTypes[] = " mutation: {$mutationType->getName()}";
}
if (null !== ($subscriptionType = $definition->getSubscriptionType())) {
$operationTypes[] = " subscription: {$subscriptionType->getName()}";
}
return printLines([
'schema {',
printLines($operationTypes),
'}'
]);
}
/**
* GraphQL schema define root types for each type of operation. These types are
* the same as any other type and can be named in any manner, however there is
* a common naming convention:
*
* schema {
* query: Query
* mutation: Mutation
* subscription: Subscription
* }
*
* When using this naming convention, the schema description can be omitted.
*
* @param Schema $schema
* @return bool
*/
protected function isSchemaOfCommonNames(Schema $schema): bool
{
if (null !== ($queryType = $schema->getQueryType()) &&
$queryType->getName() !== 'Query') {
return false;
}
if (null !== ($mutationType = $schema->getMutationType()) &&
$mutationType->getName() !== 'Mutation') {
return false;
}
if (null !== ($subscriptionType = $schema->getSubscriptionType()) &&
$subscriptionType->getName() !== 'Subscription') {
return false;
}
return true;
}
/**
* @param Directive $directive
* @return string
*/
public function printDirectiveDefinition(Directive $directive): string
{
$description = $this->printDescription($directive);
$name = $directive->getName();
$arguments = $this->printArguments($directive->getArguments());
$locations = implode(' | ', $directive->getLocations());
return printLines([
$description,
"directive @{$name}{$arguments} on {$locations}",
]);
}
/**
* @param NamedTypeInterface $type
* @return string
* @throws PrintException
* @throws InvariantException
*/
protected function printType(NamedTypeInterface $type): string
{
if ($type instanceof ScalarType) {
return $this->printScalarType($type);
}
if ($type instanceof ObjectType) {
return $this->printObjectType($type);
}
if ($type instanceof InterfaceType) {
return $this->printInterfaceType($type);
}
if ($type instanceof UnionType) {
return $this->printUnionType($type);
}
if ($type instanceof EnumType) {
return $this->printEnumType($type);
}
if ($type instanceof InputObjectType) {
return $this->printInputObjectType($type);
}
throw new PrintException(\sprintf('Unknown type: %s', (string)$type));
}
/**
* @param ScalarType $type
* @return string
*/
protected function printScalarType(ScalarType $type): string
{
return printLines([
$this->printDescription($type),
"scalar {$type->getName()}"
]);
}
/**
* @param ObjectType $type
* @return string
* @throws InvariantException
*/
protected function printObjectType(ObjectType $type): string
{
$description = $this->printDescription($type);
$name = $type->getName();
$implements = $type->hasInterfaces()
? ' implements ' . printArray(' & ', \array_map(function (InterfaceType $interface) {
return $interface->getName();
}, $type->getInterfaces()))
: '';
$fields = $this->printFields($type->getFields());
return printLines([
$description,
"type {$name}{$implements} {",
$fields,
'}'
]);
}
/**
* @param InterfaceType $type
* @return string
* @throws InvariantException
*/
protected function printInterfaceType(InterfaceType $type): string
{
$description = $this->printDescription($type);
$fields = $this->printFields($type->getFields());
return printLines([
$description,
"interface {$type->getName()} {",
$fields,
'}'
]);
}
/**
* @param UnionType $type
* @return string
* @throws InvariantException
*/
protected function printUnionType(UnionType $type): string
{
$description = $this->printDescription($type);
$types = printArray(' | ', $type->getTypes());
return printLines([
$description,
"union {$type->getName()} = {$types}"
]);
}
/**
* @param EnumType $type
* @return string
* @throws InvariantException
*/
protected function printEnumType(EnumType $type): string
{
$description = $this->printDescription($type);
$values = $this->printEnumValues($type->getValues());
return printLines([
$description,
"enum {$type->getName()} {",
$values,
'}'
]);
}
/**
* @param array $values
* @return string
*/
protected function printEnumValues(array $values): string
{
// The first item is always the first in block, all item after that are not.
// This is important for getting the linebreaks correct between the items.
$firstInBlock = true;
return printLines(\array_map(function (EnumValue $value) use (&$firstInBlock): string {
$description = $this->printDescription($value, ' ', $firstInBlock);
$name = $value->getName();
$deprecated = $this->printDeprecated($value);
$enum = empty($deprecated) ? $name : "{$name} {$deprecated}";
$firstInBlock = false;
return printLines([
$description,
" {$enum}"
]);
}, $values));
}
/**
* @param InputObjectType $type
* @return string
* @throws InvariantException
*/
protected function printInputObjectType(InputObjectType $type): string
{
$description = $this->printDescription($type);
$fields = \array_map(function (InputField $field): string {
$description = $this->printDescription($field, ' ');
$inputValue = $this->printInputValue($field);
return printLines([
$description,
" {$inputValue}"
]);
}, \array_values($type->getFields()));
return printLines([
$description,
"input {$type->getName()} {",
printLines($fields),
'}'
]);
}
/**
* @param InputValueInterface $inputValue
* @return string
* @throws InvariantException
* @throws \Digia\GraphQL\Language\SyntaxErrorException
* @throws \Digia\GraphQL\Util\ConversionException
*/
protected function printInputValue(InputValueInterface $inputValue): string
{
$type = $inputValue->getType();
$name = $inputValue->getName();
$defaultValue = $inputValue->hasDefaultValue()
? printNode(ValueConverter::convert($inputValue->getDefaultValue(), $type))
: null;
return null !== $defaultValue
? "{$name}: {$type} = {$defaultValue}"
: "{$name}: {$type}";
}
/**
* @param array $fields
* @return string
*/
protected function printFields(array $fields): string
{
// The first item is always the first in block, all item after that are not.
// This is important for getting the linebreaks correct between the items.
$firstInBlock = true;
return printLines(\array_map(function (Field $field) use (&$firstInBlock): string {
$description = $this->printDescription($field, ' ', $firstInBlock);
$name = $field->getName();
$arguments = $this->printArguments($field->getArguments());
$type = (string)$field->getType();
$deprecated = $this->printDeprecated($field);
$firstInBlock = false;
return printLines([
$description,
" {$name}{$arguments}: {$type}{$deprecated}"
]);
}, $fields));
}
/**
* @param array $arguments
* @param string $indentation
* @return string
*/
protected function printArguments(array $arguments, string $indentation = ''): string
{
if (empty($arguments)) {
return '';
}
// If every arg does not have a description, print them on one line.
if (arrayEvery($arguments, function (Argument $argument): bool {
return !$argument->hasDescription();
})) {
return printInputFields(\array_map(function (Argument $argument) {
return $this->printInputValue($argument);
}, $arguments));
}
$args = \array_map(function (Argument $argument) use ($indentation) {
$description = $this->printDescription($argument, ' ');
$inputValue = $this->printInputValue($argument);
return printLines([
"{$indentation}{$description}",
" {$indentation}{$inputValue}"
]);
}, $arguments);
return printLines([
'(',
\implode(", ", $args),
$indentation . ')'
]);
}
/**
* @param DeprecationAwareInterface $fieldOrEnumValue
* @return string
* @throws InvariantException
* @throws \Digia\GraphQL\Language\SyntaxErrorException
* @throws \Digia\GraphQL\Util\ConversionException
*/
protected function printDeprecated(DeprecationAwareInterface $fieldOrEnumValue): string
{
if (!$fieldOrEnumValue->isDeprecated()) {
return '';
}
$reason = $fieldOrEnumValue->getDeprecationReason();
if (null === $reason || '' === $reason || DEFAULT_DEPRECATION_REASON === $reason) {
return '@deprecated';
}
$reasonValue = printNode(ValueConverter::convert($reason, stringType()));
return "@deprecated(reason: {$reasonValue})";
}
/**
* @param DescriptionAwareInterface $definition
* @param string $indentation
* @param bool $isFirstInBlock
* @return string
*/
protected function printDescription(
DescriptionAwareInterface $definition,
string $indentation = '',
bool $isFirstInBlock = true
): string {
// Don't print anything if the type has no description
if ($definition->getDescription() === null) {
return '';
}
$lines = descriptionLines($definition->getDescription(), 120 - \strlen($indentation));
if (isset($this->options['commentDescriptions']) && true === $this->options['commentDescriptions']) {
return $this->printDescriptionWithComments($lines, $indentation, $isFirstInBlock);
}
$text = \implode("\n", $lines);
$preferMultipleLines = \strlen($text) > 70;
$blockString = printBlockString($text, '', $preferMultipleLines);
$prefix = strlen($indentation) > 0 && !$isFirstInBlock ? "\n" . $indentation : $indentation;
return $prefix . \str_replace("\n", "\n" . $indentation, $blockString);
}
/**
* @param array $lines
* @param string $indentation
* @param bool $isFirstInBlock
* @return string
*/
protected function printDescriptionWithComments(array $lines, string $indentation, bool $isFirstInBlock): string
{
$description = \strlen($indentation) > 0 && !$isFirstInBlock ? "\n" : '';
$linesCount = \count($lines);
for ($i = 0; $i < $linesCount; $i++) {
$description .= $lines[$i] === ''
? $indentation . '#' . "\n"
: $indentation . '# ' . $lines[$i] . "\n";
}
return $description;
}
/**
* @param DefinitionInterface $definition
* @return string
* @throws PrintException
* @throws InvariantException
*/
protected function printOne(DefinitionInterface $definition): string
{
return $this->print($definition);
}
/**
* @param DefinitionInterface[] $definitions
* @return array
*/
protected function printMany(array $definitions): array
{
return \array_map(function ($definition) {
return $this->print($definition);
}, $definitions);
}
}
================================================
FILE: src/Schema/DefinitionPrinterInterface.php
================================================
schema = $schema;
$this->document = $document;
$this->typeDefinitionMap = $typeDefinitionMap;
$this->typeExtensionsMap = $typeExtensionsMap;
$this->directiveDefinitions = $directiveDefinitions;
$this->schemaExtensions = $schemaExtensions;
}
/**
* @param string $typeName
* @return bool
*/
public function hasTypeExtensions(string $typeName): bool
{
return isset($this->typeExtensionsMap[$typeName]);
}
/**
* @param string $typeName
* @return ObjectTypeExtensionNode[]|InterfaceTypeExtensionNode[]
*/
public function getTypeExtensions(string $typeName): array
{
return $this->typeExtensionsMap[$typeName] ?? [];
}
/**
* @return Schema
*/
public function getSchema(): Schema
{
return $this->schema;
}
/**
* @return DocumentNode
*/
public function getDocument(): DocumentNode
{
return $this->document;
}
/**
* @return bool
*/
public function hasTypeDefinitionMap(): bool
{
return !empty($this->typeDefinitionMap);
}
/**
* @return TypeSystemDefinitionNodeInterface[]
*/
public function getTypeDefinitionMap(): array
{
return $this->typeDefinitionMap;
}
/**
* @return bool
*/
public function hasTypeExtensionsMap(): bool
{
return !empty($this->typeExtensionsMap);
}
/**
* @return InterfaceTypeExtensionNode[][]|ObjectTypeExtensionNode[][]
*/
public function getTypeExtensionsMap()
{
return $this->typeExtensionsMap;
}
/**
* @return bool
*/
public function hasDirectiveDefinitions(): bool
{
return !empty($this->directiveDefinitions);
}
/**
* @return DirectiveDefinitionNode[]
*/
public function getDirectiveDefinitions(): array
{
return $this->directiveDefinitions;
}
/**
* @return bool
*/
public function hasSchemaExtensions(): bool
{
return !empty($this->schemaExtensions);
}
/**
* @return SchemaExtensionNode[]
*/
public function getSchemaExtensions(): array
{
return $this->schemaExtensions;
}
}
================================================
FILE: src/Schema/Extension/ExtensionContext.php
================================================
info = $info;
}
/**
* @return bool
*/
public function isSchemaExtended(): bool
{
return
$this->info->hasTypeExtensionsMap() ||
$this->info->hasTypeDefinitionMap() ||
$this->info->hasDirectiveDefinitions() ||
$this->info->hasSchemaExtensions();
}
/**
* @return ObjectType[]
* @throws SchemaExtensionException
* @throws InvariantException
*/
public function getExtendedOperationTypes(): array
{
/** @noinspection PhpUnhandledExceptionInspection */
$operationTypes = [
'query' => $this->getExtendedQueryType(),
'mutation' => $this->getExtendedMutationType(),
'subscription' => $this->getExtendedSubscriptionType(),
];
foreach ($this->info->getSchemaExtensions() as $schemaExtension) {
foreach ($schemaExtension->getOperationTypes() as $operationType) {
$operation = $operationType->getOperation();
if (isset($operationTypes[$operation])) {
throw new SchemaExtensionException(\sprintf('Must provide only one %s type in schema.', $operation));
}
$operationTypes[$operation] = $this->definitionBuilder->buildType($operationType->getType());
}
}
return $operationTypes;
}
/**
* @return TypeInterface|null
* @throws InvariantException
*/
protected function getExtendedQueryType(): ?TypeInterface
{
$existingQueryType = $this->info->getSchema()->getQueryType();
return null !== $existingQueryType
? $this->getExtendedType($existingQueryType)
: null;
}
/**
* @return TypeInterface|null
* @throws InvariantException
*/
protected function getExtendedMutationType(): ?TypeInterface
{
$existingMutationType = $this->info->getSchema()->getMutationType();
return null !== $existingMutationType
? $this->getExtendedType($existingMutationType)
: null;
}
/**
* @return TypeInterface|null
* @throws InvariantException
*/
protected function getExtendedSubscriptionType(): ?TypeInterface
{
$existingSubscriptionType = $this->info->getSchema()->getSubscriptionType();
return null !== $existingSubscriptionType
? $this->getExtendedType($existingSubscriptionType)
: null;
}
/**
* @return TypeInterface[]
*/
public function getExtendedTypes(): array
{
$extendedTypes = \array_map(function ($type) {
return $this->getExtendedType($type);
}, $this->info->getSchema()->getTypeMap());
return \array_merge(
$extendedTypes,
$this->definitionBuilder->buildTypes($this->info->getTypeDefinitionMap())
);
}
/**
* @return Directive[]
* @throws InvariantException
*/
public function getExtendedDirectives(): array
{
$existingDirectives = $this->info->getSchema()->getDirectives();
if (empty($existingDirectives)) {
throw new InvariantException('schema must have default directives');
}
return \array_merge(
$existingDirectives,
\array_map(function (DirectiveDefinitionNode $node) {
return $this->definitionBuilder->buildDirective($node);
}, $this->info->getDirectiveDefinitions())
);
}
/**
* @param DefinitionBuilderInterface $definitionBuilder
* @return ExtensionContext
*/
public function setDefinitionBuilder(DefinitionBuilderInterface $definitionBuilder): ExtensionContext
{
$this->definitionBuilder = $definitionBuilder;
return $this;
}
/**
* @param NamedTypeNode $node
* @return TypeInterface|null
* @throws SchemaExtensionException
* @throws InvariantException
*/
public function resolveType(NamedTypeNode $node): ?TypeInterface
{
$typeName = $node->getNameValue();
$existingType = $this->info->getSchema()->getType($typeName);
if ($existingType instanceof NamedTypeInterface) {
return $this->getExtendedType($existingType);
}
throw new SchemaExtensionException(
\sprintf(
'Unknown type: "%s". Ensure that this type exists ' .
'either in the original schema, or is added in a type definition.',
$typeName
),
[$node]
);
}
/**
* @param NamedTypeInterface $type
* @return TypeInterface
* @throws InvariantException
*/
protected function getExtendedType(NamedTypeInterface $type): TypeInterface
{
$typeName = $type->getName();
if (isset($this->extendTypeCache[$typeName])) {
return $this->extendTypeCache[$typeName];
}
return $this->extendTypeCache[$typeName] = $this->extendType($type);
}
/**
* @param TypeInterface $type
* @return TypeInterface
* @throws InvariantException
*/
protected function extendType(TypeInterface $type): TypeInterface
{
/** @noinspection PhpParamsInspection */
if (isIntrospectionType($type)) {
// Introspection types are not extended.
return $type;
}
if ($type instanceof ObjectType) {
return $this->extendObjectType($type);
}
if ($type instanceof InterfaceType) {
return $this->extendInterfaceType($type);
}
if ($type instanceof UnionType) {
return $this->extendUnionType($type);
}
// This type is not yet extendable.
return $type;
}
/**
* @param ObjectType $type
* @return ObjectType
* @throws InvariantException
*/
protected function extendObjectType(ObjectType $type): ObjectType
{
$typeName = $type->getName();
$extensionASTNodes = $type->getExtensionAstNodes();
if ($this->info->hasTypeExtensions($typeName)) {
$extensionASTNodes = $this->extendExtensionASTNodes($typeName, $extensionASTNodes);
}
return newObjectType([
'name' => $typeName,
'description' => $type->getDescription(),
'interfaces' => function () use ($type) {
return $this->extendImplementedInterfaces($type);
},
'fields' => function () use ($type) {
return $this->extendFieldMap($type);
},
'astNode' => $type->getAstNode(),
'extensionASTNodes' => $extensionASTNodes,
'isTypeOf' => $type->getIsTypeOfCallback(),
]);
}
/**
* @param InterfaceType $type
* @return InterfaceType
* @throws InvariantException
*/
protected function extendInterfaceType(InterfaceType $type): InterfaceType
{
$typeName = $type->getName();
$extensionASTNodes = $this->info->getTypeExtensions($typeName);
if ($this->info->hasTypeExtensions($typeName)) {
$extensionASTNodes = $this->extendExtensionASTNodes($typeName, $extensionASTNodes);
}
return newInterfaceType([
'name' => $typeName,
'description' => $type->getDescription(),
'fields' => function () use ($type) {
return $this->extendFieldMap($type);
},
'astNode' => $type->getAstNode(),
'extensionASTNodes' => $extensionASTNodes,
'resolveType' => $type->getResolveTypeCallback(),
]);
}
/**
* @param string $typeName
* @param array $nodes
* @return array
*/
protected function extendExtensionASTNodes(string $typeName, array $nodes): array
{
$typeExtensions = $this->info->getTypeExtensions($typeName);
return !empty($nodes) ? \array_merge($typeExtensions, $nodes) : $typeExtensions;
}
/**
* @param UnionType $type
* @return UnionType
* @throws InvariantException
*/
protected function extendUnionType(UnionType $type): UnionType
{
return newUnionType([
'name' => $type->getName(),
'description' => $type->getDescription(),
'types' => \array_map(function ($unionType) {
return $this->getExtendedType($unionType);
}, $type->getTypes()),
'astNode' => $type->getAstNode(),
'resolveType' => $type->getResolveTypeCallback(),
]);
}
/**
* @param ObjectType $type
* @return array
* @throws InvariantException
*/
protected function extendImplementedInterfaces(ObjectType $type): array
{
$typeName = $type->getName();
$interfaces = \array_map(function (InterfaceType $interface) {
return $this->getExtendedType($interface);
}, $type->getInterfaces());
// If there are any extensions to the interfaces, apply those here.
$extensions = $this->info->getTypeExtensions($typeName);
foreach ($extensions as $extension) {
foreach ($extension->getInterfaces() as $namedType) {
// 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.
$interfaces[] = $this->definitionBuilder->buildType($namedType);
}
}
return $interfaces;
}
/**
* @param FieldsAwareInterface $type
* @return array
* @throws InvalidTypeException
* @throws InvariantException
* @throws SchemaExtensionException
*/
protected function extendFieldMap(FieldsAwareInterface $type): array
{
$typeName = $type->getName();
$newFieldMap = [];
$oldFieldMap = $type->getFields();
foreach (\array_keys($oldFieldMap) as $fieldName) {
$field = $oldFieldMap[$fieldName];
$newFieldMap[$fieldName] = [
'description' => $field->getDescription(),
'deprecationReason' => $field->getDeprecationReason(),
'type' => $this->extendFieldType($field->getType()),
'args' => $field->getRawArguments(),
'astNode' => $field->getAstNode(),
'resolve' => $field->getResolveCallback(),
];
}
// If there are any extensions to the fields, apply those here.
$extensions = $this->info->getTypeExtensions($typeName);
foreach ($extensions as $extension) {
foreach ($extension->getFields() as $field) {
$fieldName = $field->getNameValue();
if (isset($oldFieldMap[$fieldName])) {
throw new SchemaExtensionException(
\sprintf(
'Field "%s.%s" already exists in the schema. ' .
'It cannot also be defined in this type extension.',
$typeName, $fieldName
),
[$field]
);
}
$newFieldMap[$fieldName] = $this->definitionBuilder->buildField($field);
}
}
return $newFieldMap;
}
/**
* @param TypeInterface $typeDefinition
* @return TypeInterface
* @throws InvalidTypeException
* @throws InvariantException
*/
protected function extendFieldType(TypeInterface $typeDefinition): TypeInterface
{
if ($typeDefinition instanceof ListType) {
return newList($this->extendFieldType($typeDefinition->getOfType()));
}
if ($typeDefinition instanceof NonNullType) {
return newNonNull($this->extendFieldType($typeDefinition->getOfType()));
}
return $this->getExtendedType($typeDefinition);
}
}
================================================
FILE: src/Schema/Extension/ExtensionContextInterface.php
================================================
createContext($schema, $document, $resolverRegistry, $options);
// If this document contains no new types, extensions, or directives then
// return the same unmodified GraphQLSchema instance.
if (!$context->isSchemaExtended()) {
return $schema;
}
$operationTypes = $context->getExtendedOperationTypes();
/** @noinspection PhpUnhandledExceptionInspection */
return newSchema([
'query' => $operationTypes['query'] ?? null,
'mutation' => $operationTypes['mutation'] ?? null,
'subscription' => $operationTypes['subscription'] ?? null,
'types' => $context->getExtendedTypes(),
'directives' => $context->getExtendedDirectives(),
'astNode' => $schema->getAstNode(),
]);
}
/**
* @inheritdoc
*/
protected function createContext(
Schema $schema,
DocumentNode $document,
?ResolverRegistryInterface $resolverRegistry,
array $options
): ExtensionContextInterface {
/** @noinspection PhpUnhandledExceptionInspection */
$info = $this->createInfo($schema, $document);
// Context has to be created in order to create the definition builder,
// because we are using its `resolveType` function to resolve types.
$context = new ExtensionContext($info);
/** @noinspection PhpUnhandledExceptionInspection */
$definitionBuilder = new DefinitionBuilder(
$info->getTypeDefinitionMap(),
$resolverRegistry,
$options['types'] ?? [],
$options['directives'] ?? [],
[$context, 'resolveType']
);
return $context->setDefinitionBuilder($definitionBuilder);
}
/**
* @param Schema $schema
* @param DocumentNode $document
* @return ExtendInfo
* @throws SchemaExtensionException
*/
protected function createInfo(Schema $schema, DocumentNode $document): ExtendInfo
{
$typeDefinitionMap = [];
$typeExtensionsMap = [];
$directiveDefinitions = [];
$schemaExtensions = [];
foreach ($document->getDefinitions() as $definition) {
if ($definition instanceof SchemaDefinitionNode) {
// Sanity check that a schema extension is not defining a new schema
throw new SchemaExtensionException('Cannot define a new schema within a schema extension.', [$definition]);
}
if ($definition instanceof SchemaExtensionNode) {
$schemaExtensions[] = $definition;
continue;
}
if ($definition instanceof DirectiveDefinitionNode) {
$directiveName = $definition->getNameValue();
$existingDirective = $schema->getDirective($directiveName);
if (null !== $existingDirective) {
throw new SchemaExtensionException(
\sprintf(
'Directive "%s" already exists in the schema. It cannot be redefined.',
$directiveName
),
[$definition]
);
}
$directiveDefinitions[] = $definition;
continue;
}
if ($definition instanceof TypeSystemDefinitionNodeInterface && $definition instanceof NameAwareInterface) {
// Sanity check that none of the defined types conflict with the schema's existing types.
$typeName = $definition->getNameValue();
$existingType = $schema->getType($typeName);
if (null !== $existingType) {
throw new SchemaExtensionException(
\sprintf(
'Type "%s" already exists in the schema. It cannot also ' .
'be defined in this type definition.',
$typeName
),
[$definition]
);
}
$typeDefinitionMap[$typeName] = $definition;
continue;
}
if ($definition instanceof ObjectTypeExtensionNode || $definition instanceof InterfaceTypeExtensionNode) {
// Sanity check that this type extension exists within the schema's existing types.
$extendedTypeName = $definition->getNameValue();
$existingType = $schema->getType($extendedTypeName);
if (null === $existingType) {
throw new SchemaExtensionException(
\sprintf(
'Cannot extend type "%s" because it does not exist in the existing schema.',
$extendedTypeName
),
[$definition]
);
}
$this->checkExtensionNode($existingType, $definition);
$typeExtensionsMap[$extendedTypeName] = \array_merge(
$typeExtensionsMap[$extendedTypeName] ?? [],
[$definition]
);
continue;
}
if ($definition instanceof ScalarTypeExtensionNode ||
$definition instanceof UnionTypeExtensionNode ||
$definition instanceof EnumTypeExtensionNode ||
$definition instanceof InputObjectTypeExtensionNode) {
throw new SchemaExtensionException(
\sprintf('The %s kind is not yet supported by extendSchema().', $definition->getKind())
);
}
}
return new ExtendInfo(
$schema,
$document,
$typeDefinitionMap,
$typeExtensionsMap,
$directiveDefinitions,
$schemaExtensions
);
}
/**
* @param TypeInterface $type
* @param NodeInterface $node
* @throws SchemaExtensionException
*/
protected function checkExtensionNode(TypeInterface $type, NodeInterface $node): void
{
if ($node instanceof ObjectTypeExtensionNode && !($type instanceof ObjectType)) {
throw new SchemaExtensionException(
\sprintf('Cannot extend non-object type "%s".', toString($type)),
[$node]
);
}
if ($node instanceof InterfaceTypeExtensionNode && !($type instanceof InterfaceType)) {
throw new SchemaExtensionException(
\sprintf('Cannot extend non-interface type "%s".', toString($type)),
[$node]
);
}
}
}
================================================
FILE: src/Schema/Extension/SchemaExtenderInterface.php
================================================
container->add(SchemaExtenderInterface::class, SchemaExtender::class);
}
}
================================================
FILE: src/Schema/Resolver/AbstractFieldResolver.php
================================================
resolve(...\func_get_args());
}
/**
* @inheritdoc
*/
public function getResolveCallback(): ?callable
{
return function ($rootValue, array $arguments, $context = null, ?ResolveInfo $info = null) {
return $this->resolve($rootValue, $arguments, $context, $info);
};
}
}
================================================
FILE: src/Schema/Resolver/AbstractTypeResolver.php
================================================
getResolver($fieldName);
};
}
/**
* @param string $fieldName
* @return callable|null
*/
public function getResolver(string $fieldName): ?callable
{
$resolveMethod = 'resolve' . \ucfirst($fieldName);
if (\method_exists($this, $resolveMethod)) {
return [$this, $resolveMethod];
}
return null;
}
}
================================================
FILE: src/Schema/Resolver/ResolverCollectionInterface.php
================================================
registerResolvers($resolvers);
}
/**
* @param string $fieldName
* @param callable $resolver
*/
public function addResolver(string $fieldName, callable $resolver)
{
$this->resolvers[$fieldName] = $resolver;
}
/**
* @param string $fieldName
* @return callable|null
*/
public function getResolver(string $fieldName): ?callable
{
return $this->resolvers[$fieldName] ?? null;
}
/**
* @inheritdoc
*/
public function getResolveCallback(): ?callable
{
return function (string $fieldName) {
$resolver = $this->getResolver($fieldName);
return $resolver instanceof ResolverInterface
? $resolver->getResolveCallback()
: $resolver;
};
}
/**
* @inheritdoc
*/
public function getTypeResolver(): ?callable
{
return $this->resolvers[static::TYPE_RESOLVER_KEY] ?? null;
}
/**
* @inheritdoc
*/
public function getMiddleware(): ?array
{
return null;
}
/**
* @param array $resolvers
*/
protected function registerResolvers(array $resolvers): void
{
foreach ($resolvers as $typeName => $resolver) {
if (\is_array($resolver)) {
$resolver = new ResolverMap($resolver);
}
$this->addResolver($typeName, $resolver);
}
}
}
================================================
FILE: src/Schema/Resolver/ResolverMiddlewareInterface.php
================================================
middleware = $middleware;
$this->registerResolvers($resolvers);
}
/**
* @inheritdoc
*/
public function register(string $typeName, ResolverInterface $resolver): void
{
$this->resolverMap[$typeName] = $resolver;
}
/**
* @inheritdoc
*/
public function getFieldResolver(string $typeName, string $fieldName): ?callable
{
$resolver = $this->getResolver($typeName);
$resolver = $resolver instanceof ResolverCollectionInterface
? $resolver->getResolver($fieldName)
: $resolver;
$resolveCallback = $resolver instanceof ResolverInterface
? $resolver->getResolveCallback()
: $resolver;
if (null === $resolveCallback) {
return null;
}
if (null !== $this->middleware) {
$middleware = $resolver instanceof ResolverInterface
? $this->getMiddlewareToApply($resolver, $this->middleware)
: $this->middleware;
return $this->applyMiddleware($resolveCallback, \array_reverse($middleware));
}
return $resolveCallback;
}
/**
* @inheritdoc
*/
public function getTypeResolver(string $typeName): ?callable
{
$resolver = $this->getResolver($typeName);
if ($resolver instanceof ResolverInterface) {
return $resolver->getTypeResolver();
}
return null;
}
/**
* @inheritdoc
*/
public function getResolver(string $typeName): ?ResolverInterface
{
return $this->resolverMap[$typeName] ?? null;
}
/**
* @param array $resolvers
*/
protected function registerResolvers(array $resolvers): void
{
foreach ($resolvers as $typeName => $resolver) {
if (\is_array($resolver)) {
$resolver = new ResolverMap($resolver);
}
$this->register($typeName, $resolver);
}
}
/**
* @param ResolverInterface $resolver
* @param ResolverMiddlewareInterface[] $middleware
* @return array
*/
protected function getMiddlewareToApply(ResolverInterface $resolver, array $middleware): array
{
$resolverMiddleware = $resolver->getMiddleware();
if (null === $resolverMiddleware) {
return $middleware;
}
return \array_filter($middleware, function (ResolverMiddlewareInterface $mw) use ($resolverMiddleware) {
return \in_array(\get_class($mw), $resolverMiddleware, true);
});
}
/**
* @param callable $resolveCallback
* @param array $middleware
* @return callable
*/
protected function applyMiddleware(callable $resolveCallback, array $middleware): callable
{
return \array_reduce(
$middleware,
function (callable $resolveCallback, ResolverMiddlewareInterface $middleware) {
return function ($rootValue, array $arguments, $context = null, ?ResolveInfo $info = null) use (
$resolveCallback,
$middleware
) {
return $middleware->resolve($resolveCallback, $rootValue, $arguments, $context, $info);
};
},
$resolveCallback
);
}
}
================================================
FILE: src/Schema/Resolver/ResolverRegistryInterface.php
================================================
resolveType($rootValue, $context, $info);
};
}
/**
* @param mixed $rootValue
* @param mixed $context
* @param ResolveInfo|null $info
* @return callable|null
*/
public function resolveType($rootValue, $context = null, ?ResolveInfo $info = null): ?callable
{
// Override this method when your resolver returns an interface or an union type.
return null;
}
/**
* @return array|null
*/
public function getMiddleware(): ?array
{
return null;
}
}
================================================
FILE: src/Schema/Schema.php
================================================
$MyAppQueryRootType,
* 'mutation' => $MyAppMutationRootType,
* ])
*
* Note: If an array of `directives` are provided to GraphQLSchema, that will be
* the exact list of directives represented and allowed. If `directives` is not
* provided then a default set of the specified directives (e.g. @include and
* @skip) will be used. If you wish to provide *additional* directives to these
* specified directives, you must explicitly declare them. Example:
*
* $MyAppSchema = GraphQLSchema([
* ...
* 'directives' => \array_merge(specifiedDirectives(), [$myCustomDirective]),
* ])
*/
class Schema implements DefinitionInterface
{
use ExtensionASTNodesTrait;
use ASTNodeTrait;
/**
* @var ObjectType|null
*/
protected $queryType;
/**
* @var ObjectType|null
*/
protected $mutationType;
/**
* @var ObjectType|null
*/
protected $subscriptionType;
/**
* @var TypeInterface[]
*/
protected $types = [];
/**
* @var array
*/
protected $directives = [];
/**
* @var bool
*/
protected $assumeValid = false;
/**
* @var TypeInterface[]
*/
protected $typeMap = [];
/**
* @var array
*/
protected $implementations = [];
/**
* @var NamedTypeInterface[]
*/
protected $possibleTypesMap = [];
/**
* Schema constructor.
*
* @param ObjectType|null $queryType
* @param ObjectType|null $mutationType
* @param ObjectType|null $subscriptionType
* @param TypeInterface[] $types
* @param Directive[] $directives
* @param bool $assumeValid
* @param SchemaDefinitionNode|null $astNode
* @param ObjectTypeExtensionNode[]|InterfaceTypeExtensionNode[] $extensionASTNodes
* @throws InvariantException
*/
public function __construct(
?ObjectType $queryType,
?ObjectType $mutationType,
?ObjectType $subscriptionType,
array $types,
array $directives,
bool $assumeValid,
?SchemaDefinitionNode $astNode,
array $extensionASTNodes
) {
$this->queryType = $queryType;
$this->mutationType = $mutationType;
$this->subscriptionType = $subscriptionType;
$this->types = $types;
$this->directives = !empty($directives)
? $directives
: specifiedDirectives();
$this->assumeValid = $assumeValid;
$this->astNode = $astNode;
$this->extensionAstNodes = $extensionASTNodes;
$this->buildTypeMap();
$this->buildImplementations();
}
/**
* @return ObjectType|null
*/
public function getQueryType(): ?ObjectType
{
return $this->queryType;
}
/**
* @return ObjectType|null
*/
public function getMutationType(): ?ObjectType
{
return $this->mutationType;
}
/**
* @return ObjectType|null
*/
public function getSubscriptionType(): ?ObjectType
{
return $this->subscriptionType;
}
/**
* @param string $name
* @return Directive|null
*/
public function getDirective(string $name): ?Directive
{
return find($this->directives, function (Directive $directive) use ($name) {
return $directive->getName() === $name;
});
}
/**
* @return array
*/
public function getDirectives(): array
{
return $this->directives;
}
/**
* @return array
*/
public function getTypeMap(): array
{
return $this->typeMap;
}
/**
* @return bool
*/
public function getAssumeValid(): bool
{
return $this->assumeValid;
}
/**
* @param NamedTypeInterface $abstractType
* @param NamedTypeInterface $possibleType
* @return bool
* @throws InvariantException
*/
public function isPossibleType(NamedTypeInterface $abstractType, NamedTypeInterface $possibleType): bool
{
$abstractTypeName = $abstractType->getName();
$possibleTypeName = $possibleType->getName();
if (!isset($this->possibleTypesMap[$abstractTypeName])) {
$possibleTypes = $this->getPossibleTypes($abstractType);
if (!\is_array($possibleTypes)) {
throw new InvariantException(\sprintf(
'Could not find possible implementing types for %s ' .
'in schema. Check that schema.types is defined and is an array of ' .
'all possible types in the schema.',
$abstractTypeName
));
}
$this->possibleTypesMap[$abstractTypeName] = \array_reduce(
$possibleTypes,
function (array $map, NamedTypeInterface $type) {
$map[$type->getName()] = true;
return $map;
},
[]
);
}
return isset($this->possibleTypesMap[$abstractTypeName][$possibleTypeName]);
}
/**
* @param NamedTypeInterface $abstractType
* @return NamedTypeInterface[]|null
* @throws InvariantException
*/
public function getPossibleTypes(NamedTypeInterface $abstractType): ?array
{
if ($abstractType instanceof UnionType) {
return $abstractType->getTypes();
}
return $this->implementations[$abstractType->getName()] ?? null;
}
/**
* @param string $name
* @return TypeInterface|null
*/
public function getType(string $name): ?TypeInterface
{
return $this->typeMap[$name] ?? null;
}
/**
*
*/
protected function buildTypeMap(): void
{
$initialTypes = [
$this->queryType,
$this->mutationType,
$this->subscriptionType,
__Schema(), // Introspection schema
];
if (!empty($this->types)) {
$initialTypes = \array_merge($initialTypes, $this->types);
}
// Keep track of all types referenced within the schema.
$typeMap = [];
// First by deeply visiting all initial types.
$typeMap = \array_reduce($initialTypes, [$this, 'typeMapReducer'], $typeMap);
// Then by deeply visiting all directive types.
$typeMap = \array_reduce($this->directives, [$this, 'typeMapDirectiveReducer'], $typeMap);
// Storing the resulting map for reference by the schema.
$this->typeMap = $typeMap;
}
/**
* @throws InvariantException
*/
protected function buildImplementations(): void
{
$implementations = [];
// Keep track of all implementations by interface name.
foreach ($this->typeMap as $typeName => $type) {
if ($type instanceof ObjectType) {
foreach ($type->getInterfaces() as $interface) {
if (!($interface instanceof InterfaceType)) {
continue;
}
$interfaceName = $interface->getName();
if (!isset($implementations[$interfaceName])) {
$implementations[$interfaceName] = [];
}
$implementations[$interfaceName][] = $type;
}
}
}
$this->implementations = $implementations;
}
/**
* @param array $map
* @param TypeInterface|null $type
* @return array
* @throws InvariantException
*/
protected function typeMapReducer(array $map, ?TypeInterface $type): array
{
if (null === $type) {
return $map;
}
if ($type instanceof WrappingTypeInterface) {
return $this->typeMapReducer($map, $type->getOfType());
}
if ($type instanceof NamedTypeInterface) {
$typeName = $type->getName();
if (isset($map[$typeName])) {
if ($type !== $map[$typeName]) {
throw new InvariantException(\sprintf(
'Schema must contain unique named types but contains multiple types named "%s".',
$type->getName()
));
}
return $map;
}
$map[$typeName] = $type;
$reducedMap = $map;
if ($type instanceof UnionType) {
$reducedMap = \array_reduce($type->getTypes(), [$this, 'typeMapReducer'], $reducedMap);
}
if ($type instanceof ObjectType) {
$reducedMap = \array_reduce($type->getInterfaces(), [$this, 'typeMapReducer'], $reducedMap);
}
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
foreach ($type->getFields() as $field) {
if ($field->hasArguments()) {
$fieldArgTypes = \array_map(function (Argument $argument) {
return $argument->getType();
}, $field->getArguments());
$reducedMap = \array_reduce($fieldArgTypes, [$this, 'typeMapReducer'], $reducedMap);
}
$reducedMap = $this->typeMapReducer($reducedMap, $field->getType());
}
}
if ($type instanceof InputObjectType) {
foreach ($type->getFields() as $field) {
$reducedMap = $this->typeMapReducer($reducedMap, $field->getType());
}
}
return $reducedMap;
}
return $map;
}
/**
* @param array $map
* @param Directive $directive
* @return array
* @throws InvariantException
*/
protected function typeMapDirectiveReducer(array $map, Directive $directive): array
{
if (!$directive->hasArguments()) {
return $map;
}
return \array_reduce($directive->getArguments(), function ($map, Argument $argument) {
return $this->typeMapReducer($map, $argument->getType());
}, $map);
}
}
================================================
FILE: src/Schema/Validation/Rule/AbstractRule.php
================================================
context = $context;
return $this;
}
}
================================================
FILE: src/Schema/Validation/Rule/DirectivesRule.php
================================================
context->getSchema()->getDirectives();
foreach ($directives as $directive) {
if (!($directive instanceof Directive)) {
$this->context->reportError(
new SchemaValidationException(
\sprintf('Expected directive but got: %s.', toString($directive)),
$directive instanceof ASTNodeAwareInterface ? [$directive->getAstNode()] : null
)
);
return;
}
// Ensure they are named correctly.
$this->validateName($directive);
// TODO: Ensure proper locations.
// Ensure the arguments are valid.
$argumentNames = [];
foreach ($directive->getArguments() as $argument) {
$argumentName = $argument->getName();
// Ensure they are named correctly.
$this->validateName($argument);
// Ensure they are unique per directive.
if (isset($argumentNames[$argumentName])) {
$this->context->reportError(
new SchemaValidationException(
\sprintf(
'Argument @%s(%s:) can only be defined once.',
$directive->getName(),
$argumentName
),
$this->getAllDirectiveArgumentNodes($directive, $argumentName)
)
);
continue;
}
$argumentNames[$argumentName] = true;
// Ensure the type is an input type.
if (!isInputType($argument->getType())) {
$this->context->reportError(
new SchemaValidationException(
\sprintf(
'The type of @%s(%s:) must be Input Type but got: %s.',
$directive->getName(),
$argumentName,
(string)$argument->getType()
),
$this->getAllDirectiveArgumentNodes($directive, $argumentName)
)
);
}
}
}
}
/**
* @param Directive $directive
* @param string $argumentName
* @return array
*/
protected function getAllDirectiveArgumentNodes(Directive $directive, string $argumentName)
{
$nodes = [];
/** @var DirectiveNode|null $directiveNode */
$directiveNode = $directive->getAstNode();
if (null !== $directiveNode) {
foreach ($directiveNode->getArguments() as $node) {
if ($node->getNameValue() === $argumentName) {
$nodes[] = $node;
}
}
}
return $nodes;
}
/**
* @param mixed $node
*/
protected function validateName($node): void
{
// Ensure names are valid, however introspection types opt out.
$error = NameHelper::isValidError($node->getName(), $node);
if (null !== $error) {
$this->context->reportError($error);
}
}
}
================================================
FILE: src/Schema/Validation/Rule/RootTypesRule.php
================================================
context->getSchema();
$rootTypes = [
'query' => $schema->getQueryType(),
'mutation' => $schema->getMutationType(),
'subscription' => $schema->getSubscriptionType(),
];
foreach ($rootTypes as $operation => $rootType) {
$this->validateRootType($rootType, $operation);
}
}
/**
* @param NamedTypeInterface|ObjectType|null $rootType
* @param string $operation
*/
protected function validateRootType(?NamedTypeInterface $rootType, string $operation): void
{
$schema = $this->context->getSchema();
if ($operation === 'query' && null === $rootType) {
$this->context->reportError(
new SchemaValidationException(
\sprintf('%s root type must be provided.', \ucfirst($operation)),
$schema->hasAstNode() ? [$schema->getAstNode()] : null
)
);
return;
}
}
}
================================================
FILE: src/Schema/Validation/Rule/RuleInterface.php
================================================
context->getSchema()->getTypeMap();
foreach ($typeMap as $type) {
if (!($type instanceof NamedTypeInterface)) {
$this->context->reportError(
new SchemaValidationException(
\sprintf('Expected GraphQL named type but got: %s.', toString($type)),
$type instanceof ASTNodeAwareInterface ? [$type->getAstNode()] : null
)
);
continue;
}
// Ensure it is named correctly (excluding introspection types).
/** @noinspection PhpParamsInspection */
if (!isIntrospectionType($type)) {
$this->validateName($type);
}
if ($type instanceof ObjectType) {
// Ensure fields are valid.
$this->validateFields($type);
// Ensure objects implement the interfaces they claim to.
$this->validateObjectInterfaces($type);
continue;
}
if ($type instanceof InterfaceType) {
// Ensure fields are valid.
$this->validateFields($type);
continue;
}
if ($type instanceof UnionType) {
// Ensure Unions include valid member types.
$this->validateUnionMembers($type);
continue;
}
if ($type instanceof EnumType) {
// Ensure Enums have valid values.
$this->validateEnumValues($type);
continue;
}
if ($type instanceof InputObjectType) {
// Ensure Input Object fields are valid.
$this->validateInputFields($type);
continue;
}
}
}
/**
* @param FieldsAwareInterface $type
* @throws InvariantException
*/
protected function validateFields(FieldsAwareInterface $type): void
{
$fields = $type->getFields();
// Objects and Interfaces both must define one or more fields.
if (empty($fields)) {
$this->context->reportError(
new SchemaValidationException(
\sprintf('Type %s must define one or more fields.', $type->getName()),
$this->getAllObjectOrInterfaceNodes($type)
)
);
}
foreach ($fields as $fieldName => $field) {
// Ensure they are named correctly.
$this->validateName($field);
// Ensure they were defined at most once.
$fieldNodes = $this->getAllFieldNodes($type, $fieldName);
if (\count($fieldNodes) > 1) {
$this->context->reportError(
new SchemaValidationException(
\sprintf('Field %s.%s can only be defined once.', $type->getName(), $fieldName),
$fieldNodes
)
);
return; // continue loop
}
$fieldType = $field->getType();
// Ensure the type is an output type
if (!isOutputType($fieldType)) {
$fieldTypeNode = $this->getFieldTypeNode($type, $fieldName);
$this->context->reportError(
new SchemaValidationException(
\sprintf(
'The type of %s.%s must be Output Type but got: %s.',
$type->getName(),
$fieldName,
toString($fieldType)
),
[$fieldTypeNode]
)
);
}
// Ensure the arguments are valid
$argumentNames = [];
foreach ($field->getArguments() as $argument) {
$argumentName = $argument->getName();
// Ensure they are named correctly.
$this->validateName($argument);
// Ensure they are unique per field.
if (isset($argumentNames[$argumentName])) {
$this->context->reportError(
new SchemaValidationException(
\sprintf(
'Field argument %s.%s(%s:) can only be defined once.',
$type->getName(),
$field->getName(),
$argumentName
),
$this->getAllFieldArgumentNodes($type, $fieldName, $argumentName)
)
);
}
$argumentNames[$argumentName] = true;
// Ensure the type is an input type
if (!isInputType($argument->getType())) {
$this->context->reportError(
new SchemaValidationException(
\sprintf(
'The type of %s.%s(%s:) must be Input Type but got: %s.',
$type->getName(),
$fieldName,
$argumentName,
toString($argument->getType())
),
$this->getAllFieldArgumentNodes($type, $fieldName, $argumentName)
)
);
}
}
}
}
/**
* @param ObjectType $objectType
* @throws InvariantException
*/
protected function validateObjectInterfaces(ObjectType $objectType): void
{
$implementedTypeNames = [];
foreach ($objectType->getInterfaces() as $interface) {
if (!($interface instanceof InterfaceType)) {
$this->context->reportError(
new SchemaValidationException(
\sprintf(
'Type %s must only implement Interface types, it cannot implement %s.',
toString($objectType),
toString($interface)
),
null !== $interface
? [$this->getImplementsInterfaceNode($objectType, $interface->getName())]
: null
)
);
continue;
}
$interfaceName = $interface->getName();
if (isset($implementedTypeNames[$interfaceName])) {
$this->context->reportError(
new SchemaValidationException(
\sprintf('Type %s can only implement %s once.', $objectType->getName(), $interfaceName),
$this->getAllImplementsInterfaceNodes($objectType, $interfaceName)
)
);
continue;
}
$implementedTypeNames[$interfaceName] = true;
$this->validateObjectImplementsInterface($objectType, $interface);
}
}
/**
* @param ObjectType $objectType
* @param InterfaceType $interfaceType
* @throws InvariantException
*/
protected function validateObjectImplementsInterface(ObjectType $objectType, InterfaceType $interfaceType): void
{
$objectFields = $objectType->getFields();
$interfaceFields = $interfaceType->getFields();
// Assert each interface field is implemented.
foreach (\array_keys($interfaceFields) as $fieldName) {
$interfaceField = $interfaceFields[$fieldName];
$objectField = $objectFields[$fieldName] ?? null;
// Assert interface field exists on object.
if (null === $objectField) {
$this->context->reportError(
new SchemaValidationException(
\sprintf(
'Interface field %s.%s expected but %s does not provide it.',
$interfaceType->getName(),
$fieldName,
$objectType->getName()
),
[$this->getFieldNode($interfaceType, $fieldName), $objectType->getAstNode()]
)
);
continue;
}
// Assert interface field type is satisfied by object field type, by being
// a valid subtype. (covariant)
if (!TypeHelper::isTypeSubtypeOf(
$this->context->getSchema(), $objectField->getType(), $interfaceField->getType())) {
$this->context->reportError(
new SchemaValidationException(
\sprintf(
'Interface field %s.%s expects type %s but %s.%s is type %s.',
$interfaceType->getName(),
$fieldName,
toString($interfaceField->getType()),
$objectType->getName(),
$fieldName,
toString($objectField->getType())
),
[
$this->getFieldTypeNode($interfaceType, $fieldName),
$this->getFieldTypeNode($objectType, $fieldName),
]
)
);
}
// Assert each interface field arg is implemented.
foreach ($interfaceField->getArguments() as $interfaceArgument) {
$argumentName = $interfaceArgument->getName();
$objectArgument = find($objectField->getArguments(), function (Argument $argument) use ($argumentName) {
return $argument->getName() === $argumentName;
});
// Assert interface field arg exists on object field.
if (null === $objectArgument) {
$this->context->reportError(
new SchemaValidationException(
\sprintf(
'Interface field argument %s.%s(%s:) expected but %s.%s does not provide it.',
$interfaceType->getName(),
$fieldName,
$argumentName,
$objectType->getName(),
$fieldName
),
[
$this->getFieldArgumentNode($interfaceType, $fieldName, $argumentName),
$this->getFieldNode($objectType, $fieldName),
]
)
);
continue;
}
// Assert interface field arg type matches object field arg type.
// (invariant)
// TODO: change to contravariant?
if (!TypeHelper::isEqualType($interfaceArgument->getType(), $objectArgument->getType())) {
$this->context->reportError(
new SchemaValidationException(
\sprintf(
'Interface field argument %s.%s(%s:) expects type %s but %s.%s(%s:) is type %s.',
$interfaceType->getName(),
$fieldName,
$argumentName,
toString($interfaceArgument->getType()),
$objectType->getName(),
$fieldName,
$argumentName,
toString($objectArgument->getType())
),
[
$this->getFieldArgumentTypeNode($interfaceType, $fieldName, $argumentName),
$this->getFieldArgumentTypeNode($objectType, $fieldName, $argumentName),
]
)
);
continue;
}
// TODO: validate default values?
foreach ($objectField->getArguments() as $objectArgument) {
$argumentName = $objectArgument->getName();
$interfaceArgument = find(
$interfaceField->getArguments(),
function (Argument $argument) use ($argumentName) {
return $argument->getName() === $argumentName;
}
);
if (null === $interfaceArgument && $objectArgument->getType() instanceof NonNullType) {
$this->context->reportError(
new SchemaValidationException(
\sprintf(
'Object field argument %s.%s(%s:) is of required type %s ' .
'but is not also provided by the Interface field %s.%s.',
$objectType->getName(),
$fieldName,
$argumentName,
toString($objectArgument->getType()),
$interfaceType->getName(),
$fieldName
),
[
$this->getFieldArgumentNode($objectType, $fieldName, $argumentName),
$this->getFieldNode($interfaceType, $fieldName),
]
)
);
continue;
}
}
}
}
}
/**
* @param UnionType $unionType
* @throws InvariantException
*/
protected function validateUnionMembers(UnionType $unionType): void
{
$memberTypes = $unionType->getTypes();
if (empty($memberTypes)) {
$this->context->reportError(
new SchemaValidationException(
sprintf('Union type %s must define one or more member types.', $unionType->getName()),
[$unionType->getAstNode()]
)
);
}
$includedTypeNames = [];
foreach ($memberTypes as $memberType) {
$memberTypeName = (string)$memberType;
if (isset($includedTypeNames[$memberTypeName])) {
$this->context->reportError(
new SchemaValidationException(
\sprintf(
'Union type %s can only include type %s once.',
$unionType->getName(),
$memberTypeName
),
$this->getUnionMemberTypeNodes($unionType, $memberTypeName)
)
);
continue;
}
$includedTypeNames[$memberTypeName] = true;
if (!($memberType instanceof ObjectType)) {
$this->context->reportError(
new SchemaValidationException(
\sprintf(
'Union type %s can only include Object types, it cannot include %s.',
$unionType->getName(),
toString($memberType)
),
$this->getUnionMemberTypeNodes($unionType, $memberTypeName)
)
);
}
}
}
/**
* @param EnumType $enumType
* @throws InvariantException
*/
protected function validateEnumValues(EnumType $enumType): void
{
$enumValues = $enumType->getValues();
if (empty($enumValues)) {
$this->context->reportError(
new SchemaValidationException(
\sprintf('Enum type %s must define one or more values.', $enumType->getName()),
[$enumType->getAstNode()]
)
);
}
foreach ($enumValues as $enumValue) {
$valueName = $enumValue->getName();
// Ensure no duplicates.
$allNodes = $this->getEnumValueNodes($enumType, $valueName);
if (null !== $allNodes && \count($allNodes) > 1) {
$this->context->reportError(
new SchemaValidationException(
sprintf('Enum type %s can include value %s only once.', $enumType->getName(), $valueName),
$allNodes
)
);
continue;
}
// Ensure valid name.
$this->validateName($enumValue);
if ($valueName === 'true' || $valueName === 'false' || $valueName === 'null') {
$this->context->reportError(
new SchemaValidationException(
sprintf('Enum type %s cannot include value: %s.', $enumType->getName(), $valueName),
[$enumValue->getAstNode()]
)
);
continue;
}
}
}
/**
* @param InputObjectType $inputObjectType
* @throws InvariantException
*/
protected function validateInputFields(InputObjectType $inputObjectType): void
{
$fields = $inputObjectType->getFields();
if (empty($fields)) {
$this->context->reportError(
new SchemaValidationException(
\sprintf('Input Object type %s must define one or more fields.', $inputObjectType->getName()),
[$inputObjectType->getAstNode()]
)
);
}
// Ensure the arguments are valid
foreach ($fields as $fieldName => $field) {
// Ensure they are named correctly.
$this->validateName($field);
// TODO: Ensure they are unique per field.
// Ensure the type is an input type
if (!isInputType($field->getType())) {
$this->context->reportError(
new SchemaValidationException(
\sprintf(
'The type of %s.%s must be Input Type but got: %s.',
$inputObjectType->getName(),
$fieldName,
toString($field->getType())
),
[$field->getAstNode()]
)
);
}
}
}
/**
* @param FieldsAwareInterface $type
* @return ObjectTypeDefinitionNode[]|ObjectTypeExtensionNode[]|InterfaceTypeDefinitionNode[]
* |InterfaceTypeExtensionNode[]
*/
protected function getAllObjectOrInterfaceNodes(FieldsAwareInterface $type): array
{
$node = $type->getAstNode();
$extensionASTNodes = $type->getExtensionAstNodes();
if (null !== $node) {
return !empty($extensionASTNodes)
? \array_merge([$node], $extensionASTNodes)
: [$node];
}
return $extensionASTNodes;
}
/**
* @param FieldsAwareInterface $type
* @param string $fieldName
* @return TypeNodeInterface|null
*/
protected function getFieldTypeNode(FieldsAwareInterface $type, string $fieldName): ?TypeNodeInterface
{
$fieldNode = $this->getFieldNode($type, $fieldName);
return null !== $fieldNode ? $fieldNode->getType() : null;
}
/**
* @param FieldsAwareInterface $type
* @param string $fieldName
* @return FieldDefinitionNode|null
*/
protected function getFieldNode(FieldsAwareInterface $type, string $fieldName): ?FieldDefinitionNode
{
return $this->getAllFieldNodes($type, $fieldName)[0] ?? null;
}
/**
* @param FieldsAwareInterface $type
* @param string $fieldName
* @return FieldDefinitionNode[]
*/
protected function getAllFieldNodes(FieldsAwareInterface $type, string $fieldName): array
{
$nodes = [];
foreach ($this->getAllObjectOrInterfaceNodes($type) as $objectOrInterface) {
foreach ($objectOrInterface->getFields() as $node) {
if ($node->getNameValue() === $fieldName) {
$nodes[] = $node;
}
}
}
return $nodes;
}
/**
* @param FieldsAwareInterface $type
* @param string $fieldName
* @param string $argumentName
* @return InputValueDefinitionNode|null
*/
protected function getFieldArgumentNode(
FieldsAwareInterface $type,
string $fieldName,
string $argumentName
): ?InputValueDefinitionNode {
return $this->getAllFieldArgumentNodes($type, $fieldName, $argumentName)[0] ?? null;
}
/**
* @param FieldsAwareInterface $type
* @param string $fieldName
* @param string $argumentName
* @return InputValueDefinitionNode[]
*/
protected function getAllFieldArgumentNodes(
FieldsAwareInterface $type,
string $fieldName,
string $argumentName
): array {
$nodes = [];
$fieldNode = $this->getFieldNode($type, $fieldName);
if (null !== $fieldNode) {
foreach ($fieldNode->getArguments() as $node) {
if ($node->getNameValue() === $argumentName) {
$nodes[] = $node;
}
}
}
return $nodes;
}
/**
* @param ObjectType $type
* @param string $interfaceName
* @return NamedTypeNode|null
*/
protected function getImplementsInterfaceNode(ObjectType $type, string $interfaceName): ?NamedTypeNode
{
return $this->getAllImplementsInterfaceNodes($type, $interfaceName)[0] ?? null;
}
/**
* @param ObjectType $type
* @param string $interfaceName
* @return NamedTypeNode[]
*/
protected function getAllImplementsInterfaceNodes(ObjectType $type, string $interfaceName): array
{
$nodes = [];
foreach ($this->getAllObjectOrInterfaceNodes($type) as $object) {
foreach ($object->getInterfaces() as $node) {
if ($node->getNameValue() === $interfaceName) {
$nodes[] = $node;
}
}
}
return $nodes;
}
/**
* @param FieldsAwareInterface $type
* @param string $fieldName
* @param string $argumentName
* @return TypeNodeInterface|null
*/
protected function getFieldArgumentTypeNode(
FieldsAwareInterface $type,
string $fieldName,
string $argumentName
): ?TypeNodeInterface {
$node = $this->getFieldArgumentNode($type, $fieldName, $argumentName);
return null !== $node ? $node->getType() : null;
}
/**
* @param UnionType $unionType
* @param string $memberTypeName
* @return array|null
*/
protected function getUnionMemberTypeNodes(UnionType $unionType, string $memberTypeName): ?array
{
/** @var UnionTypeDefinitionNode|null $node */
$node = $unionType->getAstNode();
if (null === $node) {
return null;
}
return \array_filter($node->getTypes(), function (NamedTypeNode $type) use ($memberTypeName) {
return $type->getNameValue() === $memberTypeName;
});
}
/**
* @param EnumType $enumType
* @param string $valueName
* @return array|null
*/
protected function getEnumValueNodes(EnumType $enumType, string $valueName): ?array
{
/** @var EnumTypeDefinitionNode|null $node */
$node = $enumType->getAstNode();
if (null === $node) {
return null;
}
return \array_filter($node->getValues(), function (NameAwareInterface $type) use ($valueName) {
return $type->getNameValue() === $valueName;
});
}
/**
* @param mixed $node
*/
protected function validateName($node): void
{
// Ensure names are valid, however introspection types opt out.
$error = NameHelper::isValidError($node->getName(), $node);
if (null !== $error) {
$this->context->reportError($error);
}
}
}
================================================
FILE: src/Schema/Validation/SchemaValidationException.php
================================================
container->share(SchemaValidatorInterface::class, SchemaValidator::class);
// Rules
$this->container->add(RootTypesRule::class, RootTypesRule::class);
$this->container->add(DirectivesRule::class, DirectivesRule::class);
$this->container->add(TypesRule::class, TypesRule::class);
}
}
================================================
FILE: src/Schema/Validation/SchemaValidator.php
================================================
createContext($schema);
$rules = $rules ?? SupportedRules::build();
foreach ($rules as $rule) {
$rule->setContext($context)->evaluate();
}
return $context->getErrors();
}
/**
* @inheritdoc
*/
public function createContext(Schema $schema): ValidationContextInterface
{
return new ValidationContext($schema);
}
}
================================================
FILE: src/Schema/Validation/SchemaValidatorInterface.php
================================================
schema = $schema;
}
/**
* @inheritdoc
*/
public function reportError(ValidationExceptionInterface $error): void
{
$this->errors[] = $error;
}
/**
* @inheritdoc
*/
public function getErrors(): array
{
return $this->errors;
}
/**
* @inheritdoc
*/
public function getSchema(): Schema
{
return $this->schema;
}
}
================================================
FILE: src/Schema/Validation/ValidationContextInterface.php
================================================
120 character long lines, cut at space boundaries into sublines of ~80 chars.
return breakLine($line, $maxLength);
}, \explode("\n", $description)));
}
/**
* @param string $line
* @param int $maxLength
* @return array
*/
function breakLine(string $line, int $maxLength): array
{
if (\strlen($line) < ($maxLength + 5)) {
return [$line];
}
$endPos = $maxLength - 40;
return \array_map('trim', \preg_split(
"/((?: |^).{15,{$endPos}}(?= |$))/",
$line,
0,
PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE
));
}
/**
* @param string $line
* @return string
*/
function escapeQuotes(string $line): string
{
return \strtr($line, ['"""' => '\\"""', '`' => '\`']);
}
/**
* @param array $lines
* @return string
*/
function printLines(array $lines): string
{
// Don't print empty lines
$lines = \array_filter($lines, function (string $line) {
return $line !== '';
});
return printArray("\n", $lines);
}
/**
* @param array $fields
* @return string
*/
function printInputFields(array $fields): string
{
return '(' . printArray(', ', $fields) . ')';
}
/**
* @param string $glue
* @param array $items
* @return string
*/
function printArray(string $glue, array $items): string
{
return \implode($glue, $items);
}
================================================
FILE: src/Type/Coercer/AbstractCoercer.php
================================================
PHP_INT_MAX || $value < PHP_INT_MIN) {
throw new InvalidTypeException(
\sprintf('Int cannot represent non 32-bit signed integer value: %s', $value)
);
}
$floatValue = (float)$value;
if ($floatValue != $value || \floor($floatValue) !== $floatValue) {
throw new InvalidTypeException(\sprintf('Int cannot represent non-integer value: %s', $value));
}
return (int)$value;
}
}
================================================
FILE: src/Type/Coercer/StringCoercer.php
================================================
container->share(BooleanCoercer::class, BooleanCoercer::class);
$this->container->share(FloatCoercer::class, FloatCoercer::class);
$this->container->share(IntCoercer::class, IntCoercer::class);
$this->container->share(StringCoercer::class, StringCoercer::class);
}
}
================================================
FILE: src/Type/Definition/AbstractTypeInterface.php
================================================
name = $name;
$this->description = $description;
$this->type = $type;
$this->defaultValue = $defaultValue;
$this->astNode = $astNode;
}
}
================================================
FILE: src/Type/Definition/ArgumentsAwareInterface.php
================================================
rawArguments;
}
/**
* @return bool
*/
public function hasArguments(): bool
{
return !empty($this->arguments);
}
/**
* @return Argument[]
*/
public function getArguments(): array
{
return $this->arguments;
}
/**
* @param string $typeName
* @param array $rawArguments
* @return Argument[]
* @throws InvariantException
*/
protected function buildArguments(string $typeName, array $rawArguments): array
{
if (!isAssocArray($rawArguments)) {
throw new InvariantException(\sprintf(
'%s.%s args must be an object with argument names as keys.',
$typeName,
$this->getName()
));
}
$arguments = [];
foreach ($rawArguments as $argumentName => $argumentConfig) {
$argumentConfig['name'] = $argumentName;
$arguments[] = newArgument($argumentConfig);
}
return $arguments;
}
}
================================================
FILE: src/Type/Definition/CompositeTypeInterface.php
================================================
defaultValue;
}
/**
* @return mixed|null
*/
public function getDefaultValue()
{
return $this->defaultValue;
}
}
================================================
FILE: src/Type/Definition/DeprecationAwareInterface.php
================================================
deprecationReason;
}
/**
* @return bool
*/
public function isDeprecated(): bool
{
return null !== $this->deprecationReason;
}
}
================================================
FILE: src/Type/Definition/DescriptionAwareInterface.php
================================================
description;
}
/**
* @return null|string
*/
public function getDescription(): ?string
{
return $this->description;
}
}
================================================
FILE: src/Type/Definition/Directive.php
================================================
name = $name;
$this->description = $description;
$this->locations = $locations;
$this->rawArguments = $rawArguments;
$this->astNode = $astNode;
$this->arguments = $this->buildArguments($typeName, $this->rawArguments);
}
/**
* @return string[]
*/
public function getLocations(): array
{
return $this->locations;
}
}
================================================
FILE: src/Type/Definition/EnumType.php
================================================
'RGB',
* 'values' => [
* 'RED' => ['value' => 0],
* 'GREEN' => ['value' => 1],
* 'BLUE' => ['value' => 2]
* ]
* ]);
*
* Note: If a value is not provided in a definition, the name of the enum value
* will be used as its internal value.
*/
class EnumType implements NamedTypeInterface, InputTypeInterface, LeafTypeInterface,
OutputTypeInterface, SerializableTypeInterface, DescriptionAwareInterface, ASTNodeAwareInterface
{
use NameTrait;
use DescriptionTrait;
use ASTNodeTrait;
/**
* @var array
*/
protected $rawValues;
/**
* A list of enum value instances.
*
* @var EnumValue[]
*/
protected $values;
/**
* EnumType constructor.
*
* @param string $name
* @param null|string $description
* @param EnumValue[] $rawValues
* @param EnumTypeDefinitionNode|null $astNode
*/
public function __construct(string $name, ?string $description, array $rawValues, ?EnumTypeDefinitionNode $astNode)
{
$this->name = $name;
$this->description = $description;
$this->astNode = $astNode;
$this->rawValues = $rawValues;
}
/**
* @param mixed $value
* @return null|string
* @throws InvariantException
*/
public function serialize($value)
{
$enumValue = $this->getValueByValue($value);
if ($enumValue) {
return $enumValue->getName();
}
return null;
}
/**
* @param mixed $value
* @return mixed|null
* @throws InvariantException
*/
public function parseValue($value)
{
if (\is_string($value)) {
$enumValue = $this->getValueByName($value);
if ($enumValue !== null) {
return $enumValue->getValue();
}
}
return null;
}
/**
* @param NodeInterface $node
* @return mixed|null
* @throws InvariantException
*/
public function parseLiteral(NodeInterface $node)
{
if ($node instanceof EnumValueNode) {
$enumValue = $this->getValueByName($node->getValue());
if ($enumValue !== null) {
return $enumValue->getValue();
}
}
return null;
}
/**
* @param string $name
* @return EnumValue|null
* @throws InvariantException
*/
public function getValue(string $name): ?EnumValue
{
return $this->getValueByName($name);
}
/**
* @return EnumValue[]
* @throws InvariantException
*/
public function getValues(): array
{
if (!isset($this->values)) {
$this->values = $this->buildValues($this->rawValues);
}
return $this->values;
}
/**
* @param string $name
* @return EnumValue|null
* @throws InvariantException
*/
protected function getValueByName(string $name): ?EnumValue
{
foreach ($this->getValues() as $enumValue) {
if ($enumValue->getName() === $name) {
return $enumValue;
}
}
return null;
}
/**
* @param mixed $value
* @return EnumValue|null
* @throws InvariantException
*/
protected function getValueByValue($value): ?EnumValue
{
foreach ($this->getValues() as $enumValue) {
if ($enumValue->getValue() === $value) {
return $enumValue;
}
}
return null;
}
/**
* @param array $rawValues
* @return array
* @throws InvariantException
*/
protected function buildValues(array $rawValues): array
{
if (!isAssocArray($rawValues)) {
throw new InvariantException(\sprintf('%s values must be an associative array with value names as keys.',
$this->name));
}
$values = [];
foreach ($rawValues as $valueName => $valueConfig) {
if (!isAssocArray($valueConfig)) {
throw new InvariantException(\sprintf(
'%s.%s must refer to an object with a "value" key representing an internal value but got: %s.',
$this->name,
$valueName,
toString($valueConfig)
));
}
if (isset($valueConfig['isDeprecated'])) {
throw new InvariantException(\sprintf(
'%s.%s should provide "deprecationReason" instead of "isDeprecated".',
$this->name,
$valueName
));
}
$valueConfig['name'] = $valueName;
$valueConfig['value'] = \array_key_exists('value', $valueConfig)
? $valueConfig['value']
: $valueName;
$values[] = newEnumValue($valueConfig);
}
return $values;
}
}
================================================
FILE: src/Type/Definition/EnumValue.php
================================================
name = $name;
$this->description = $description;
$this->deprecationReason = $deprecationReason;
$this->astNode = $astNode;
$this->value = $value;
}
}
================================================
FILE: src/Type/Definition/ExtensionASTNodesTrait.php
================================================
extensionAstNodes);
}
/**
* @return ObjectTypeExtensionNode[]|InterfaceTypeExtensionNode[]
*/
public function getExtensionAstNodes(): array
{
return $this->extensionAstNodes;
}
}
================================================
FILE: src/Type/Definition/Field.php
================================================
name = $name;
$this->description = $description;
$this->type = $type;
$this->rawArguments = $rawArguments;
$this->resolveCallback = $resolveCallback;
$this->subscribeCallback = $subscribeCallback;
$this->deprecationReason = $deprecationReason;
$this->astNode = $astNode;
$this->arguments = $this->buildArguments($typeName, $this->rawArguments);
}
/**
* @param array ...$args
* @return mixed
*/
public function subscribe(...$args)
{
return null !== $this->subscribeCallback
? \call_user_func_array($this->subscribeCallback, $args)
: null;
}
/**
* @return bool
*/
public function hasSubscribeCallback()
{
return null !== $this->subscribeCallback;
}
/**
* @return callable|null
*/
public function getSubscribeCallback(): ?callable
{
return $this->subscribeCallback;
}
}
================================================
FILE: src/Type/Definition/FieldInterface.php
================================================
getFields()[$fieldName] ?? null;
}
/**
* @return Field[]
* @throws InvariantException
*/
public function getFields(): array
{
// Fields are built lazily to avoid concurrency issues.
if (!isset($this->fieldMap)) {
$this->fieldMap = $this->buildFieldMap($this->rawFieldsOrThunk);
}
return $this->fieldMap;
}
/**
* @param array|callable $rawFieldsOrThunk
* @return Field[]
* @throws InvariantException
*/
protected function buildFieldMap($rawFieldsOrThunk): array
{
$rawFields = resolveThunk($rawFieldsOrThunk);
if (!isAssocArray($rawFields)) {
throw new InvariantException(\sprintf(
'%s fields must be an associative array with field names as keys or a ' .
'callable which returns such an array.',
$this->getName()
));
}
$fieldMap = [];
foreach ($rawFields as $fieldName => $fieldConfig) {
if (!\is_array($fieldConfig)) {
throw new InvariantException(\sprintf('%s.%s field config must be an associative array.',
$this->getName(), $fieldName));
}
if (isset($fieldConfig['isDeprecated'])) {
throw new InvariantException(\sprintf(
'%s.%s should provide "deprecationReason" instead of "isDeprecated".',
$this->getName(),
$fieldName
));
}
if (isset($fieldConfig['resolve']) && !\is_callable($fieldConfig['resolve'])) {
throw new InvariantException(\sprintf(
'%s.%s field resolver must be a function if provided, but got: %s.',
$this->getName(),
$fieldName,
toString($fieldConfig['resolve'])
));
}
$fieldConfig['name'] = $fieldName;
$fieldConfig['typeName'] = $this->getName();
$fieldMap[$fieldName] = newField($fieldConfig);
}
return $fieldMap;
}
}
================================================
FILE: src/Type/Definition/InputField.php
================================================
name = $name;
$this->type = $type;
$this->defaultValue = $defaultValue;
$this->description = $description;
$this->astNode = $astNode;
}
}
================================================
FILE: src/Type/Definition/InputObjectType.php
================================================
newNonNull(floatType())],
* 'lon': ['type' => newNonNull(floatType())],
* 'alt': ['type' => floatType(), 'defaultValue' => 0],
* ]
* ]);
*/
class InputObjectType implements NamedTypeInterface, InputTypeInterface, DescriptionAwareInterface,
ASTNodeAwareInterface
{
use NameTrait;
use DescriptionTrait;
use ASTNodeTrait;
/**
* Fields can be defined either as an array or as a thunk.
* Using thunks allows for cross-referencing of fields.
*
* @var array|callable
*/
protected $rawFieldsOrThunk;
/**
* A key-value map over field names and their corresponding field instances.
*
* @var InputField[]
*/
protected $fieldMap;
/**
* InputObjectType constructor.
*
* @param string $name
* @param null|string $description
* @param array|callable $rawFieldsOrThunk
* @param InputObjectTypeDefinitionNode|null $astNode
*/
public function __construct(
string $name,
?string $description,
$rawFieldsOrThunk,
?InputObjectTypeDefinitionNode $astNode
) {
$this->name = $name;
$this->description = $description;
$this->rawFieldsOrThunk = $rawFieldsOrThunk;
$this->astNode = $astNode;
}
/**
* @param string $fieldName
* @return InputField|null
* @throws InvariantException
*/
public function getField(string $fieldName): ?InputField
{
return $this->getFields()[$fieldName] ?? null;
}
/**
* @return InputField[]
* @throws InvariantException
*/
public function getFields(): array
{
if (!isset($this->fieldMap)) {
$this->fieldMap = $this->buildFieldMap($this->rawFieldsOrThunk);
}
return $this->fieldMap;
}
/**
* @param array|callable $rawFieldsOrThunk
* @return array
* @throws InvariantException
*/
protected function buildFieldMap($rawFieldsOrThunk): array
{
$rawFields = resolveThunk($rawFieldsOrThunk);
if (!isAssocArray($rawFields)) {
throw new InvariantException(\sprintf(
'%s fields must be an associative array with field names as keys or a function which returns such an array.',
$this->name
));
}
$fieldMap = [];
foreach ($rawFields as $fieldName => $fieldConfig) {
if (isset($fieldConfig['resolve'])) {
throw new InvariantException(\sprintf(
'%s.%s field type has a resolve property, but Input Types cannot define resolvers.',
$this->name,
$fieldName
));
}
$fieldConfig['name'] = $fieldName;
$fieldMap[$fieldName] = newInputField($fieldConfig);
}
return $fieldMap;
}
}
================================================
FILE: src/Type/Definition/InputTypeInterface.php
================================================
'Entity',
* 'fields' => [
* 'name' => ['type' => stringType()]
* ]
* ]);
*/
class InterfaceType implements AbstractTypeInterface, CompositeTypeInterface,
OutputTypeInterface, DescriptionAwareInterface, FieldsAwareInterface, ASTNodeAwareInterface
{
use NameTrait;
use DescriptionTrait;
use FieldsTrait;
use ResolveTypeTrait;
use ASTNodeTrait;
use ExtensionASTNodesTrait;
/**
* InterfaceType constructor.
*
* @param string $name
* @param null|string $description
* @param array|callable $rawFieldsOrThunk
* @param callable|null $resolveTypeCallback
* @param InterfaceTypeDefinitionNode|null $astNode
* @param InterfaceTypeExtensionNode[] $extensionASTNodes
*/
public function __construct(
string $name,
?string $description,
$rawFieldsOrThunk,
?callable $resolveTypeCallback,
?InterfaceTypeDefinitionNode $astNode,
array $extensionASTNodes
) {
$this->name = $name;
$this->description = $description;
$this->rawFieldsOrThunk = $rawFieldsOrThunk;
$this->resolveTypeCallback = $resolveTypeCallback;
$this->astNode = $astNode;
$this->extensionAstNodes = $extensionASTNodes;
}
}
================================================
FILE: src/Type/Definition/LeafTypeInterface.php
================================================
ofType = $ofType;
}
/**
* @inheritdoc
*/
public function __toString(): string
{
return '[' . (string)$this->ofType . ']';
}
}
================================================
FILE: src/Type/Definition/NameTrait.php
================================================
name;
}
/**
* @inheritdoc
*/
public function __toString(): string
{
return $this->name;
}
}
================================================
FILE: src/Type/Definition/NamedTypeInterface.php
================================================
ofType = $ofType;
}
/**
* @inheritdoc
*/
public function __toString(): string
{
return (string)$this->ofType . '!';
}
}
================================================
FILE: src/Type/Definition/ObjectType.php
================================================
'Address',
* 'fields' => [
* 'street' => ['type' => stringType()],
* 'number' => ['type' => intType()],
* 'formatted' => [
* 'type' => stringType(),
* 'resolve' => function ($obj) {
* return $obj->number . ' ' . $obj->street
* }
* ]
* ]
* ]);
*
* When two types need to refer to each other, or a type needs to refer to
* itself in a field, you can use a function expression (aka a closure or a
* thunk) to supply the fields lazily.
*
* Example:
*
* $PersonType = newObjectType([
* 'name' => 'Person',
* 'fields' => function () {
* return [
* 'name' => ['type' => stringType()],
* 'bestFriend' => ['type' => $PersonType],
* ];
* }
* ]);
*/
class ObjectType implements NamedTypeInterface, CompositeTypeInterface, OutputTypeInterface,
ASTNodeAwareInterface, DescriptionAwareInterface, FieldsAwareInterface
{
use NameTrait;
use DescriptionTrait;
use FieldsTrait;
use ResolveTrait;
use ASTNodeTrait;
use ExtensionASTNodesTrait;
/**
* @var callable
*/
protected $isTypeOfCallback;
/**
* Interfaces can be defined either as an array or as a thunk.
* Using thunks allows for cross-referencing of interfaces.
*
* @var array|callable
*/
protected $interfacesOrThunk;
/**
* A list of interface instances.
*
* @var InterfaceType[]|null
*/
protected $interfaces;
/**
* ObjectType constructor.
*
* @param string $name
* @param null|string $description
* @param array|callable $fieldsOrThunk
* @param array|callable $interfacesOrThunk
* @param callable|null $isTypeOfCallback
* @param ObjectTypeDefinitionNode|null $astNode
* @param ObjectTypeExtensionNode[] $extensionASTNodes
*/
public function __construct(
string $name,
?string $description,
$fieldsOrThunk,
$interfacesOrThunk,
?callable $isTypeOfCallback,
?ObjectTypeDefinitionNode $astNode,
array $extensionASTNodes
) {
$this->name = $name;
$this->description = $description;
$this->rawFieldsOrThunk = $fieldsOrThunk;
$this->interfacesOrThunk = $interfacesOrThunk;
$this->isTypeOfCallback = $isTypeOfCallback;
$this->astNode = $astNode;
$this->extensionAstNodes = $extensionASTNodes;
}
/**
* @param mixed $value
* @param mixed $context
* @param mixed $info
* @return bool|PromiseInterface
*/
public function isTypeOf($value, $context, $info)
{
return null !== $this->isTypeOfCallback
? \call_user_func($this->isTypeOfCallback, $value, $context, $info)
: false;
}
/**
* @return bool
* @throws InvariantException
*/
public function hasInterfaces(): bool
{
return !empty($this->getInterfaces());
}
/**
* @return InterfaceType[]
* @throws InvariantException
*/
public function getInterfaces(): array
{
if ($this->interfaces === null) {
$this->interfaces = $this->buildInterfaces($this->interfacesOrThunk);
}
return $this->interfaces;
}
/**
* @return bool
*/
public function hasIsTypeOfCallback(): bool
{
return null !== $this->isTypeOfCallback;
}
/**
* @return null|callable
*/
public function getIsTypeOfCallback(): ?callable
{
return $this->isTypeOfCallback;
}
/**
* @param array|callable $interfacesOrThunk
* @return array
* @throws InvariantException
*/
protected function buildInterfaces($interfacesOrThunk): array
{
$interfaces = resolveThunk($interfacesOrThunk);
if (!\is_array($interfaces)) {
throw new InvariantException(
\sprintf('%s interfaces must be an array or a function which returns an array.', $this->name)
);
}
return $interfaces;
}
}
================================================
FILE: src/Type/Definition/OfTypeTrait.php
================================================
ofType;
}
}
================================================
FILE: src/Type/Definition/OutputTypeInterface.php
================================================
resolveCallback
? \call_user_func_array($this->resolveCallback, $args)
: null;
}
/**
* @return bool
*/
public function hasResolveCallback()
{
return null !== $this->resolveCallback;
}
/**
* @return callable|null
*/
public function getResolveCallback(): ?callable
{
return $this->resolveCallback;
}
}
================================================
FILE: src/Type/Definition/ResolveTypeTrait.php
================================================
resolveTypeCallback
? \call_user_func_array($this->resolveTypeCallback, $args)
: null;
}
/**
* @return bool
*/
public function hasResolveTypeCallback(): bool
{
return null !== $this->resolveTypeCallback;
}
/**
* @return callable|null
*/
public function getResolveTypeCallback(): ?callable
{
return $this->resolveTypeCallback;
}
}
================================================
FILE: src/Type/Definition/ScalarType.php
================================================
name = $name;
$this->description = $description;
$this->serializeCallback = $serializeCallback;
$this->parseValueCallback = $parseValueCallback;
$this->parseLiteralCallback = $parseLiteralCallback;
$this->astNode = $astNode;
}
/**
* @param mixed $value
* @return mixed
*/
public function serialize($value)
{
return \call_user_func($this->serializeCallback, $value);
}
/**
* @param mixed $value
* @return mixed|null
*/
public function parseValue($value)
{
return null !== $this->parseValueCallback
? \call_user_func($this->parseValueCallback, $value)
: null;
}
/**
* @param NodeInterface $node
* @param array|null $variables
* @return mixed|null
*/
public function parseLiteral(NodeInterface $node, ?array $variables = null)
{
return null !== $this->parseLiteralCallback
? \call_user_func($this->parseLiteralCallback, $node, $variables)
: null;
}
/**
* @param mixed $value
* @return bool
*/
public function isValidValue($value): bool
{
return null !== $this->parseValue($value);
}
/**
* @param NodeInterface $node
* @return bool
*/
public function isValidLiteral(NodeInterface $node): bool
{
return null !== $this->parseLiteral($node);
}
}
================================================
FILE: src/Type/Definition/SerializableTypeInterface.php
================================================
getConstants());
}
}
================================================
FILE: src/Type/Definition/TypeInterface.php
================================================
getConstants());
}
}
================================================
FILE: src/Type/Definition/TypeTrait.php
================================================
type;
}
}
================================================
FILE: src/Type/Definition/UnionType.php
================================================
'Pet',
* 'types' => [$DogType, $CatType],
* 'resolveType' => function ($value) {
* if ($value instanceof Dog) {
* return $DogType;
* }
* if ($value instanceof Cat) {
* return $CatType;
* }
* }
* ]);
*/
class UnionType implements AbstractTypeInterface, CompositeTypeInterface, OutputTypeInterface,
ASTNodeAwareInterface, DescriptionAwareInterface
{
use NameTrait;
use DescriptionTrait;
use ResolveTypeTrait;
use ASTNodeTrait;
/**
* Types can be defined either as an array or as a thunk.
* Using thunks allows for cross-referencing of types.
*
* @var array|callable
*/
protected $typesOrThunk;
/**
* A key-value map over type names and their corresponding type instances.
*
* @var TypeInterface[]
*/
protected $typeMap;
/**
* UnionType constructor.
*
* @param string $name
* @param null|string $description
* @param array|callable $typesOrThunk
* @param callable|null $resolveTypeCallback
* @param UnionTypeDefinitionNode|null $astNode
*/
public function __construct(
string $name,
?string $description,
$typesOrThunk,
?callable $resolveTypeCallback,
?UnionTypeDefinitionNode $astNode
) {
$this->name = $name;
$this->description = $description;
$this->typesOrThunk = $typesOrThunk;
$this->resolveTypeCallback = $resolveTypeCallback;
$this->astNode = $astNode;
}
/**
* @return NamedTypeInterface[]
* @throws InvariantException
*/
public function getTypes(): array
{
// Types are built lazily to avoid concurrency issues.
if (!isset($this->typeMap)) {
$this->typeMap = $this->buildTypeMap($this->typesOrThunk);
}
return $this->typeMap;
}
/**
* @param array|callable $typesOrThunk
* @return array
* @throws InvariantException
*/
protected function buildTypeMap($typesOrThunk): array
{
$typeMap = resolveThunk($typesOrThunk);
if (!\is_array($typeMap)) {
throw new InvariantException(\sprintf(
'Must provide array of types or a function which returns such an array for Union %s.',
$this->name
));
}
return $typeMap;
}
}
================================================
FILE: src/Type/Definition/ValueTrait.php
================================================
value;
}
}
================================================
FILE: src/Type/Definition/WrappingTypeInterface.php
================================================
container
->share(GraphQL::INCLUDE_DIRECTIVE, function () {
return newDirective([
'name' => 'include',
'description' =>
'Directs the executor to include this field or fragment only when ' .
'the `if` argument is true.',
'locations' => [
DirectiveLocationEnum::FIELD,
DirectiveLocationEnum::FRAGMENT_SPREAD,
DirectiveLocationEnum::INLINE_FRAGMENT,
],
'args' => [
'if' => [
'type' => newNonNull(booleanType()),
'description' => 'Included when true.',
],
],
]);
});
$this->container
->share(GraphQL::SKIP_DIRECTIVE, function () {
return newDirective([
'name' => 'skip',
'description' =>
'Directs the executor to skip this field or fragment when the `if` ' .
'argument is true.',
'locations' => [
DirectiveLocationEnum::FIELD,
DirectiveLocationEnum::FRAGMENT_SPREAD,
DirectiveLocationEnum::INLINE_FRAGMENT,
],
'args' => [
'if' => [
'type' => newNonNull(booleanType()),
'description' => 'Skipped when true.',
],
],
]);
});
$this->container
->share(GraphQL::DEPRECATED_DIRECTIVE, function () {
return newDirective([
'name' => 'deprecated',
'description' => 'Marks an element of a GraphQL schema as no longer supported.',
'locations' => [
DirectiveLocationEnum::FIELD_DEFINITION,
DirectiveLocationEnum::ENUM_VALUE,
],
'args' => [
'reason' => [
'type' => stringType(),
'description' =>
'Explains why this element was deprecated, usually also including a suggestion ' .
'for how to access supported similar data. Formatted using the Markdown syntax ' .
'(as specified by [CommonMark](https://commonmark.org/).',
'defaultValue' => DEFAULT_DEPRECATION_REASON,
],
]
]);
});
}
}
================================================
FILE: src/Type/IntrospectionProvider.php
================================================
registerIntrospectionTypes();
$this->registerMetaFields();
}
/**
* Registers the introspection types with the container.
*/
protected function registerIntrospectionTypes()
{
$this->container
->share(GraphQL::SCHEMA_INTROSPECTION, function () {
return newObjectType([
'name' => GraphQL::SCHEMA_INTROSPECTION,
'isIntrospection' => true,
'description' =>
'A GraphQL Schema defines the capabilities of a GraphQL server. It ' .
'exposes all available types and directives on the server, as well as ' .
'the entry points for query, mutation, and subscription operations.',
'fields' => function () {
return [
'types' => [
'description' => 'A list of all types supported by this server.',
'type' => newNonNull(newList(newNonNull(__Type()))),
'resolve' => function (Schema $schema): array {
return \array_values($schema->getTypeMap());
},
],
'queryType' => [
'description' => 'The type that query operations will be rooted at.',
'type' => newNonNull(__Type()),
'resolve' => function (Schema $schema): ?TypeInterface {
return $schema->getQueryType();
},
],
'mutationType' => [
'description' =>
'If this server supports mutation, the type that ' .
'mutation operations will be rooted at.',
'type' => __Type(),
'resolve' => function (Schema $schema): ?TypeInterface {
return $schema->getMutationType();
},
],
'subscriptionType' => [
'description' =>
'If this server support subscription, the type that ' .
'subscription operations will be rooted at.',
'type' => __Type(),
'resolve' => function (Schema $schema): ?TypeInterface {
return $schema->getSubscriptionType();
},
],
'directives' => [
'description' => 'A list of all directives supported by this server.',
'type' => newNonNull(newList(newNonNull(__Directive()))),
'resolve' => function (Schema $schema): array {
return $schema->getDirectives();
},
],
];
}
]);
});
$this->container
->share(GraphQL::DIRECTIVE_INTROSPECTION, function () {
return newObjectType([
'name' => GraphQL::DIRECTIVE_INTROSPECTION,
'isIntrospection' => true,
'description' =>
'A Directive provides a way to describe alternate runtime execution and ' .
'type validation behavior in a GraphQL document.' .
"\n\nIn some cases, you need to provide options to alter GraphQL's " .
'execution behavior in ways field arguments will not suffice, such as ' .
'conditionally including or skipping a field. Directives provide this by ' .
'describing additional information to the executor.',
'fields' => function () {
return [
'name' => ['type' => newNonNull(stringType())],
'description' => ['type' => stringType()],
'locations' => [
'type' => newNonNull(newList(newNonNull(__DirectiveLocation()))),
],
'args' => [
'type' => newNonNull(newList(newNonNull(__InputValue()))),
'resolve' => function (Directive $directive): array {
return $directive->getArguments() ?: [];
},
],
];
}
]);
});
$this->container
->share(GraphQL::DIRECTIVE_LOCATION_INTROSPECTION, function () {
return newEnumType([
'name' => GraphQL::DIRECTIVE_LOCATION_INTROSPECTION,
'isIntrospection' => true,
'description' =>
'A Directive can be adjacent to many parts of the GraphQL language, a ' .
'__DirectiveLocation describes one such possible adjacencies.',
'values' => [
DirectiveLocationEnum::QUERY => [
'description' => 'Location adjacent to a query operation.',
],
DirectiveLocationEnum::MUTATION => [
'description' => 'Location adjacent to a mutation operation.',
],
DirectiveLocationEnum::SUBSCRIPTION => [
'description' => 'Location adjacent to a subscription operation.',
],
DirectiveLocationEnum::FIELD => [
'description' => 'Location adjacent to a field.',
],
DirectiveLocationEnum::FRAGMENT_DEFINITION => [
'description' => 'Location adjacent to a fragment definition.',
],
DirectiveLocationEnum::FRAGMENT_SPREAD => [
'description' => 'Location adjacent to a fragment spread.',
],
DirectiveLocationEnum::INLINE_FRAGMENT => [
'description' => 'Location adjacent to an inline fragment.',
],
DirectiveLocationEnum::VARIABLE_DEFINITION => [
'description' => 'Location adjacent to a variable definition.',
],
DirectiveLocationEnum::SCHEMA => [
'description' => 'Location adjacent to a schema definition.',
],
DirectiveLocationEnum::SCALAR => [
'description' => 'Location adjacent to a scalar definition.',
],
DirectiveLocationEnum::OBJECT => [
'description' => 'Location adjacent to an object type definition.',
],
DirectiveLocationEnum::FIELD_DEFINITION => [
'description' => 'Location adjacent to a field definition.',
],
DirectiveLocationEnum::ARGUMENT_DEFINITION => [
'description' => 'Location adjacent to an argument definition.',
],
DirectiveLocationEnum::INTERFACE => [
'description' => 'Location adjacent to an interface definition.',
],
DirectiveLocationEnum::UNION => [
'description' => 'Location adjacent to a union definition.',
],
DirectiveLocationEnum::ENUM => [
'description' => 'Location adjacent to an enum definition.',
],
DirectiveLocationEnum::ENUM_VALUE => [
'description' => 'Location adjacent to an enum value definition.',
],
DirectiveLocationEnum::INPUT_OBJECT => [
'description' => 'Location adjacent to an input object type definition.',
],
DirectiveLocationEnum::INPUT_FIELD_DEFINITION => [
'description' => 'Location adjacent to an input object field definition.',
],
],
]);
});
$this->container
->share(GraphQL::TYPE_INTROSPECTION, function () {
return newObjectType([
'name' => GraphQL::TYPE_INTROSPECTION,
'isIntrospection' => true,
'description' =>
'The fundamental unit of any GraphQL Schema is the type. There are many kinds of ' .
"types in GraphQL as represented by the `__TypeKind` enum.\n\n" .
'Depending on the kind of a type, certain fields describe information about that ' .
'type. Scalar types provide no information beyond a name and description, while ' .
'Enum types provide their values. Object and Interface types provide the fields ' .
'they describe. Abstract types, Union and Interface, provide the Object types ' .
'possible at runtime. List and NonNull types compose other types.',
'fields' => function () {
return [
'kind' => [
'type' => newNonNull(__TypeKind()),
'resolve' => function (TypeInterface $type) {
if ($type instanceof ScalarType) {
return TypeKindEnum::SCALAR;
}
if ($type instanceof ObjectType) {
return TypeKindEnum::OBJECT;
}
if ($type instanceof InterfaceType) {
return TypeKindEnum::INTERFACE;
}
if ($type instanceof UnionType) {
return TypeKindEnum::UNION;
}
if ($type instanceof EnumType) {
return TypeKindEnum::ENUM;
}
if ($type instanceof InputObjectType) {
return TypeKindEnum::INPUT_OBJECT;
}
if ($type instanceof ListType) {
return TypeKindEnum::LIST;
}
if ($type instanceof NonNullType) {
return TypeKindEnum::NON_NULL;
}
throw new InvalidTypeException(\sprintf('Unknown kind of type: %s', (string)$type));
},
],
'name' => ['type' => stringType()],
'description' => ['type' => stringType()],
'fields' => [
'type' => newList(newNonNull(__Field())),
'args' => [
'includeDeprecated' => ['type' => booleanType(), 'defaultValue' => false],
],
'resolve' => function (TypeInterface $type, array $args):
?array {
['includeDeprecated' => $includeDeprecated] = $args;
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
$fields = \array_values($type->getFields());
if (!$includeDeprecated) {
$fields = \array_filter($fields, function (Field $field) {
return !$field->isDeprecated();
});
}
return $fields;
}
return null;
},
],
'interfaces' => [
'type' => newList(newNonNull(__Type())),
'resolve' => function (TypeInterface $type): ?array {
return $type instanceof ObjectType ? $type->getInterfaces() : null;
},
],
'possibleTypes' => [
'type' => newList(newNonNull(__Type())),
'resolve' => function (
TypeInterface $type,
array $args,
array $context,
ResolveInfo $info
):
?array {
/** @var Schema $schema */
$schema = $info->getSchema();
/** @noinspection PhpParamsInspection */
return $type instanceof AbstractTypeInterface ? $schema->getPossibleTypes($type) : null;
},
],
'enumValues' => [
'type' => newList(newNonNull(__EnumValue())),
'args' => [
'includeDeprecated' => ['type' => booleanType(), 'defaultValue' => false],
],
'resolve' => function (TypeInterface $type, array $args): ?array {
['includeDeprecated' => $includeDeprecated] = $args;
if ($type instanceof EnumType) {
$values = \array_values($type->getValues());
if (!$includeDeprecated) {
$values = \array_filter($values, function (Field $field) {
return !$field->isDeprecated();
});
}
return $values;
}
return null;
},
],
'inputFields' => [
'type' => newList(newNonNull(__InputValue())),
'resolve' => function (TypeInterface $type): ?array {
return $type instanceof InputObjectType ? $type->getFields() : null;
},
],
'ofType' => ['type' => __Type()],
];
}
]);
});
$this->container
->share(GraphQL::FIELD_INTROSPECTION, function () {
return newObjectType([
'name' => GraphQL::FIELD_INTROSPECTION,
'isIntrospection' => true,
'description' =>
'Object and Interface types are described by a list of Fields, each of ' .
'which has a name, potentially a list of arguments, and a return type.',
'fields' => function () {
return [
'name' => ['type' => newNonNull(stringType())],
'description' => ['type' => stringType()],
'args' => [
'type' => newNonNull(newList(newNonNull(__InputValue()))),
'resolve' => function (ArgumentsAwareInterface $directive): array {
return $directive->getArguments() ?? [];
},
],
'type' => ['type' => newNonNull(__Type())],
'isDeprecated' => ['type' => newNonNull(booleanType())],
'deprecationReason' => ['type' => stringType()],
];
}
]);
});
$this->container
->share(GraphQL::INPUT_VALUE_INTROSPECTION, function () {
return newObjectType([
'name' => GraphQL::INPUT_VALUE_INTROSPECTION,
'isIntrospection' => true,
'description' =>
'Arguments provided to Fields or Directives and the input fields of an ' .
'InputObject are represented as Input Values which describe their type ' .
'and optionally a default value.',
'fields' => function () {
return [
'name' => ['type' => newNonNull(stringType())],
'description' => ['type' => stringType()],
'type' => ['type' => newNonNull(__Type())],
'defaultValue' => [
'type' => stringType(),
'description' =>
'A GraphQL-formatted string representing the default value for this ' .
'input value.',
'resolve' => function (/*$inputValue*/) {
// TODO: Implement this when we have support for printing AST.
return null;
}
],
];
}
]);
});
$this->container
->share(GraphQL::ENUM_VALUE_INTROSPECTION, function () {
return newObjectType([
'name' => GraphQL::ENUM_VALUE_INTROSPECTION,
'isIntrospection' => true,
'description' =>
'One possible value for a given Enum. Enum values are unique values, not ' .
'a placeholder for a string or numeric value. However an Enum value is ' .
'returned in a JSON response as a string.',
'fields' => function () {
return [
'name' => ['type' => newNonNull(stringType())],
'description' => ['type' => stringType()],
'isDeprecated' => ['type' => newNonNull(booleanType())],
'deprecationReason' => ['type' => stringType()],
];
}
]);
});
$this->container
->share(GraphQL::TYPE_KIND_INTROSPECTION, function () {
return newEnumType([
'name' => GraphQL::TYPE_KIND_INTROSPECTION,
'isIntrospection' => true,
'description' => 'An enum describing what kind of type a given `__Type` is.',
'values' => [
TypeKindEnum::SCALAR => [
'description' => 'Indicates this type is a scalar.',
],
TypeKindEnum::OBJECT => [
'description' => 'Indicates this type is an object. `fields` and `interfaces` are valid fields.',
],
TypeKindEnum::INTERFACE => [
'description' => 'Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.',
],
TypeKindEnum::UNION => [
'description' => 'Indicates this type is a union. `possibleTypes` is a valid field.',
],
TypeKindEnum::ENUM => [
'description' => 'Indicates this type is an enum. `enumValues` is a valid field.',
],
TypeKindEnum::INPUT_OBJECT => [
'description' => 'Indicates this type is an input object. `inputFields` is a valid field.',
],
TypeKindEnum::LIST => [
'description' => 'Indicates this type is a list. `ofType` is a valid field.',
],
TypeKindEnum::NON_NULL => [
'description' => 'Indicates this type is a non-null. `ofType` is a valid field.',
],
],
]);
});
}
/**
* Registers the introspection meta fields with the container.
*/
protected function registerMetaFields()
{
$this->container
->share(GraphQL::SCHEMA_META_FIELD_DEFINITION, function ($__Schema) {
return newField([
'name' => '__schema',
'description' => 'Access the current type schema of this server.',
'type' => newNonNull($__Schema),
'resolve' => function ($source, $args, $context, ResolveInfo $info): Schema {
return $info->getSchema();
},
]);
})
->addArgument(GraphQL::SCHEMA_INTROSPECTION);
$this->container
->share(GraphQL::TYPE_META_FIELD_DEFINITION, function ($__Type) {
return newField([
'name' => '__type',
'description' => 'Request the type information of a single type.',
'type' => $__Type,
'args' => ['name' => ['type' => newNonNull(stringType())]],
'resolve' => function ($source, $args, $context, ResolveInfo $info): ?TypeInterface {
['name' => $name] = $args;
return $info->getSchema()->getType($name);
},
]);
})
->addArgument(GraphQL::TYPE_INTROSPECTION);
$this->container
->share(GraphQL::TYPE_NAME_META_FIELD_DEFINITION, function () {
return newField([
'name' => '__typename',
'description' => 'The name of the current Object type at runtime.',
'type' => newNonNull(stringType()),
'resolve' => function ($source, $args, $context, ResolveInfo $info): string {
return $info->getParentType()->getName();
},
]);
});
}
}
================================================
FILE: src/Type/ScalarTypesProvider.php
================================================
container
->share(GraphQL::BOOLEAN, function (BooleanCoercer $coercer) {
return newScalarType([
'name' => TypeNameEnum::BOOLEAN,
'description' => 'The `Boolean` scalar type represents `true` or `false`.',
'serialize' => function ($value) use ($coercer) {
return $coercer->coerce($value);
},
'parseValue' => function ($value) use ($coercer) {
return $coercer->coerce($value);
},
'parseLiteral' => function (NodeInterface $node) {
if ($node instanceof BooleanValueNode) {
return $node->getValue();
}
return null;
},
]);
})
->addArgument(BooleanCoercer::class);
$this->container
->share(GraphQL::FLOAT, function (FloatCoercer $coercer) {
return newScalarType([
'name' => TypeNameEnum::FLOAT,
'description' =>
'The `Float` scalar type represents signed double-precision fractional ' .
'values as specified by ' .
'[IEEE 754](https://en.wikipedia.org/wiki/IEEE_754).',
'serialize' => function ($value) use ($coercer) {
return $coercer->coerce($value);
},
'parseValue' => function ($value) use ($coercer) {
return $coercer->coerce($value);
},
'parseLiteral' => function (NodeInterface $node) {
if ($node instanceof FloatValueNode || $node instanceof IntValueNode) {
return $node->getValue();
}
return null;
},
]);
})
->addArgument(FloatCoercer::class);
$this->container
->share(GraphQL::INT, function (IntCoercer $coercer) {
return newScalarType([
'name' => TypeNameEnum::INT,
'description' =>
'The `Int` scalar type represents non-fractional signed whole numeric ' .
'values. Int can represent values between -(2^31) and 2^31 - 1.',
'serialize' => function ($value) use ($coercer) {
return $coercer->coerce($value);
},
'parseValue' => function ($value) use ($coercer) {
return $coercer->coerce($value);
},
'parseLiteral' => function (NodeInterface $node) {
if ($node instanceof IntValueNode) {
$value = (int)$node->getValue();
if ((string)$node->getValue() === (string)$value &&
$value <= PHP_INT_MAX && $value >= PHP_INT_MIN) {
return $value;
}
}
return null;
},
]);
})
->addArgument(IntCoercer::class);
$this->container
->share(GraphQL::ID, function (StringCoercer $coercer) {
return newScalarType([
'name' => TypeNameEnum::ID,
'description' =>
'The `ID` scalar type represents a unique identifier, often used to ' .
'refetch an object or as key for a cache. The ID type appears in a JSON ' .
'response as a String; however, it is not intended to be human-readable. ' .
'When expected as an input type, any string (such as `"4"`) or integer ' .
'(such as `4`) input value will be accepted as an ID.',
'serialize' => function ($value) use ($coercer) {
return $coercer->coerce($value);
},
'parseValue' => function ($value) use ($coercer) {
return $coercer->coerce($value);
},
'parseLiteral' => function (NodeInterface $node) {
if ($node instanceof StringValueNode || $node instanceof IntValueNode) {
return $node->getValue();
}
return null;
},
]);
})
->addArgument(StringCoercer::class);
$this->container
->share(GraphQL::STRING, function (StringCoercer $coercer) {
return newScalarType([
'name' => TypeNameEnum::STRING,
'description' =>
'The `String` scalar type represents textual data, represented as UTF-8 ' .
'character sequences. The String type is most often used by GraphQL to ' .
'represent free-form human-readable text.',
'serialize' => function ($value) use ($coercer) {
return $coercer->coerce($value);
},
'parseValue' => function ($value) use ($coercer) {
return $coercer->coerce($value);
},
'parseLiteral' => function (NodeInterface $node) {
if ($node instanceof StringValueNode) {
return $node->getValue();
}
return null;
},
]);
})
->addArgument(StringCoercer::class);
}
}
================================================
FILE: src/Type/TypeKindEnum.php
================================================
getConstants());
}
}
================================================
FILE: src/Type/definition.php
================================================
getOfType())));
}
/**
* Whether a type is an output type cannot be determined with `instanceof`
* because lists and non-nulls can also be output types if the wrapped type is an output type.
*
* @param TypeInterface|null $type
* @return bool
*/
function isOutputType(?TypeInterface $type): bool
{
return null !== $type &&
($type instanceof OutputTypeInterface ||
($type instanceof WrappingTypeInterface && isOutputType($type->getOfType())));
}
/**
* @param TypeInterface|null $type
* @return TypeInterface|null
*/
function getNullableType(?TypeInterface $type): ?TypeInterface
{
if (null === $type) {
return null;
}
return $type instanceof NonNullType ? $type->getOfType() : $type;
}
/**
* @param TypeInterface|null $type
* @return TypeInterface|null
*/
function getNamedType(?TypeInterface $type): ?TypeInterface
{
if (!$type) {
return null;
}
$unwrappedType = $type;
while ($unwrappedType instanceof WrappingTypeInterface) {
$unwrappedType = $unwrappedType->getOfType();
}
return $unwrappedType;
}
/**
* Returns a new Scalar type after ensuring that its state is valid.
*
* @param array $config
* @return ScalarType
* @throws InvariantException
*/
function newScalarType(array $config = []): ScalarType
{
invariant(isset($config['name']), 'Must provide name.');
invariant(
isset($config['serialize']) && \is_callable($config['serialize']),
\sprintf(
'%s must provide "serialize" function. If this custom Scalar ' .
'is also used as an input type, ensure "parseValue" and "parseLiteral" ' .
'functions are also provided.',
$config['name']
)
);
if (isset($config['parseValue']) || isset($config['parseLiteral'])) {
invariant(
(isset($config['parseValue']) && \is_callable($config['parseValue'])) &&
(isset($config['parseLiteral']) && \is_callable($config['parseLiteral'])),
\sprintf('%s must provide both "parseValue" and "parseLiteral" functions.', $config['name'])
);
}
return new ScalarType(
$config['name'],
$config['description'] ?? null,
$config['serialize'],
$config['parseValue'] ?? null,
$config['parseLiteral'] ?? null,
$config['astNode'] ?? null
);
}
/**
* Returns a new Enum type after ensuring that its state is valid.
*
* @param array $config
* @return EnumType
* @throws InvariantException
*/
function newEnumType(array $config = []): EnumType
{
invariant(isset($config['name']), 'Must provide name.');
return new EnumType(
$config['name'],
$config['description'] ?? null,
$config['values'] ?? [],
$config['astNode'] ?? null
);
}
/**
* Returns a new Enum value after ensuring that its state is valid.
*
* @param array $config
* @return EnumValue
* @throws InvariantException
*/
function newEnumValue(array $config = []): EnumValue
{
invariant(isset($config['name']), 'Must provide name.');
return new EnumValue(
$config['name'],
$config['description'] ?? null,
$config['deprecationReason'] ?? null,
$config['astNode'] ?? null,
$config['value'] ?? null
);
}
/**
* Returns a new Input Object type after ensuring that its state is valid.
*
* @param array $config
* @return InputObjectType
* @throws InvariantException
*/
function newInputObjectType(array $config = []): InputObjectType
{
invariant(isset($config['name']), 'Must provide name.');
return new InputObjectType(
$config['name'],
$config['description'] ?? null,
$config['fields'] ?? [],
$config['astNode'] ?? null
);
}
/**
* Returns a new Input field after ensuring that its state is valid.
*
* @param array $config
* @return InputField
* @throws InvariantException
*/
function newInputField(array $config = []): InputField
{
invariant(isset($config['name']), 'Must provide name.');
return new InputField(
$config['name'],
$config['description'] ?? null,
$config['type'] ?? null,
$config['defaultValue'] ?? null,
$config['astNode'] ?? null
);
}
/**
* Returns a new Interface type after ensuring that its state is valid.
*
* @param array $config
* @return InterfaceType
* @throws InvariantException
*/
function newInterfaceType(array $config = []): InterfaceType
{
invariant(isset($config['name']), 'Must provide name.');
invariant(
!isset($config['resolveType']) || null === $config['resolveType'] || \is_callable($config['resolveType']),
\sprintf('%s must provide "resolveType" as a function.', $config['name'])
);
return new InterfaceType(
$config['name'],
$config['description'] ?? null,
$config['fields'] ?? [],
$config['resolveType'] ?? null,
$config['astNode'] ?? null,
$config['extensionASTNodes'] ?? []
);
}
/**
* Returns a new Object type after ensuring that its state is valid.
*
* @param array $config
* @return ObjectType
* @throws InvariantException
*/
function newObjectType(array $config = []): ObjectType
{
invariant(isset($config['name']), 'Must provide name.');
if (isset($config['isTypeOf'])) {
invariant(
\is_callable($config['isTypeOf']),
\sprintf('%s must provide "isTypeOf" as a function.', $config['name'])
);
}
return new ObjectType(
$config['name'],
$config['description'] ?? null,
$config['fields'] ?? [],
$config['interfaces'] ?? [],
$config['isTypeOf'] ?? null,
$config['astNode'] ?? null,
$config['extensionASTNodes'] ?? []
);
}
/**
* Returns a new Field after ensuring that its state is valid.
*
* @param array $config
* @return Field
* @throws InvariantException
*/
function newField(array $config = []): Field
{
invariant(isset($config['name']), 'Must provide name.');
return new Field(
$config['name'],
$config['description'] ?? null,
$config['type'] ?? null,
$config['args'] ?? [],
$config['resolve'] ?? null,
$config['subscribe'] ?? null,
$config['deprecationReason'] ?? null,
$config['astNode'] ?? null,
$config['typeName'] ?? ''
);
}
/**
* Returns a new Argument after ensuring that its state is valid.
*
* @param array $config
* @return Argument
* @throws InvariantException
*/
function newArgument(array $config = []): Argument
{
invariant(isset($config['name']), 'Must provide name.');
return new Argument(
$config['name'],
$config['description'] ?? null,
$config['type'] ?? null,
$config['defaultValue'] ?? null,
$config['astNode'] ?? null
);
}
/**
* Returns a new Union type after ensuring that its state is valid.
*
* @param array $config
* @return UnionType
* @throws InvariantException
*/
function newUnionType(array $config = []): UnionType
{
invariant(isset($config['name']), 'Must provide name.');
if (isset($config['resolveType'])) {
invariant(
\is_callable($config['resolveType']),
\sprintf('%s must provide "resolveType" as a function.', $config['name'])
);
}
return new UnionType(
$config['name'],
$config['description'] ?? null,
$config['types'] ?? [],
$config['resolveType'] ?? null,
$config['astNode'] ?? null
);
}
/**
* Returns a new Schema after ensuring that its state is valid.
*
* @param array $config
* @return Schema
* @throws InvariantException
*/
function newSchema(array $config = []): Schema
{
if (!isset($config['assumeValid']) || !$config['assumeValid']) {
if (isset($config['types'])) {
invariant(
\is_array($config['types']),
\sprintf('"types" must be Array if provided but got: %s.', toString($config['types']))
);
}
if (isset($config['directives'])) {
invariant(
\is_array($config['directives']),
\sprintf('"directives" must be Array if provided but got: %s.', toString($config['directives']))
);
}
}
return new Schema(
$config['query'] ?? null,
$config['mutation'] ?? null,
$config['subscription'] ?? null,
$config['types'] ?? [],
$config['directives'] ?? [],
$config['assumeValid'] ?? false,
$config['astNode'] ?? null,
$config['extensionASTNodes'] ?? []
);
}
/**
* Returns a new Directive after ensuring that its state is valid.
*
* @param array $config
* @return Directive
* @throws InvariantException
*/
function newDirective(array $config = []): Directive
{
invariant(isset($config['name']), 'Must provide name.');
invariant(
isset($config['locations']) && \is_array($config['locations']),
'Must provide locations for directive.'
);
return new Directive(
$config['name'],
$config['description'] ?? null,
$config['locations'],
$config['args'] ?? [],
$config['astNode'] ?? null,
$config['typeName'] ?? ''
);
}
/**
* Returns a new List type after ensuring that its state is valid.
*
* @param mixed $ofType
* @return ListType
* @throws InvariantException
*/
function newList($ofType): ListType
{
assertType($ofType);
return new ListType($ofType);
}
/**
* Returns a new Non-null type after ensuring that its state is valid.
*
* @param mixed $ofType
* @return NonNullType
* @throws InvariantException
*/
function newNonNull($ofType): NonNullType
{
if ($ofType instanceof NonNullType) {
throw new InvariantException(\sprintf('Expected %s to be a GraphQL nullable type.', toString($ofType)));
}
return new NonNullType($ofType);
}
================================================
FILE: src/Type/directives.php
================================================
getName() === $directive->getName();
}
);
}
================================================
FILE: src/Type/introspection.php
================================================
getName() === $introspectionType->getName();
}
);
}
================================================
FILE: src/Type/scalars.php
================================================
getName() === $specifiedScalarType->getName();
}
);
}
================================================
FILE: src/Util/AbstractEnum.php
================================================
getConstants());
}
}
================================================
FILE: src/Util/ArrayToJsonTrait.php
================================================
toArray(), JSON_UNESCAPED_UNICODE);
}
}
================================================
FILE: src/Util/ConversionException.php
================================================
1 && $name[0] === '_' && $name[1] === '_') {
return new ValidationException(
sprintf('Name "%s" must not begin with "__", which is reserved by GraphQL introspection.', $name),
$node instanceof NodeInterface ? [$node] : null
);
}
if (preg_match("/^[_a-zA-Z][_a-zA-Z0-9]*$/", $name) === 0) {
return new ValidationException(
sprintf('Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "%s" does not.', $name),
$node instanceof NodeInterface ? [$node] : null
);
}
return null;
}
}
================================================
FILE: src/Util/NodeComparator.php
================================================
toJSON() === $other->toJSON();
}
}
================================================
FILE: src/Util/SerializationInterface.php
================================================
getType());
return null !== $innerType ? newList($innerType) : null;
}
if ($typeNode instanceof NonNullTypeNode) {
$innerType = self::convert($schema, $typeNode->getType());
return null !== $innerType ? newNonNull($innerType) : null;
}
if ($typeNode instanceof NamedTypeNode) {
return $schema->getType($typeNode->getNameValue());
}
throw new ConversionException(sprintf('Unexpected type kind: %s', $typeNode->getKind()));
}
}
================================================
FILE: src/Util/TypeHelper.php
================================================
getOfType(), $typeB->getOfType());
}
// If either type is a list, the other must also be a list.
if ($typeA instanceof ListType && $typeB instanceof ListType) {
return self::isEqualType($typeA->getOfType(), $typeB->getOfType());
}
// Otherwise the types are not equal.
return false;
}
/**
* 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 Schema $schema
* @param TypeInterface $maybeSubtype
* @param TypeInterface $superType
* @return bool
*
* @throws InvariantException
*/
public static function isTypeSubtypeOf(
Schema $schema,
TypeInterface $maybeSubtype,
TypeInterface $superType
): bool {
// Equivalent type is a valid subtype.
if ($maybeSubtype === $superType) {
return true;
}
// If superType is non-null, maybeSubType must also be non-null.
if ($superType instanceof NonNullType) {
if ($maybeSubtype instanceof NonNullType) {
return self::isTypeSubtypeOf($schema, $maybeSubtype->getOfType(), $superType->getOfType());
}
return false;
}
if ($maybeSubtype instanceof NonNullType) {
// If superType is nullable, maybeSubType may be non-null or nullable.
return self::isTypeSubtypeOf($schema, $maybeSubtype->getOfType(), $superType);
}
// If superType type is a list, maybeSubType type must also be a list.
if ($superType instanceof ListType) {
if ($maybeSubtype instanceof ListType) {
return self::isTypeSubtypeOf($schema, $maybeSubtype->getOfType(), $superType->getOfType());
}
return false;
}
if ($maybeSubtype instanceof ListType) {
// If superType is not a list, maybeSubType must also be not a list.
return false;
}
// If superType type is an abstract type, maybeSubType type may be a currently
// possible object type.
if ($superType instanceof AbstractTypeInterface &&
$maybeSubtype instanceof ObjectType &&
$schema->isPossibleType($superType, $maybeSubtype)) {
return true;
}
// Otherwise, the child type is not a valid subtype of the parent type.
return false;
}
/**
* Provided two composite types, determine if they "overlap". Two composite
* types overlap when the Sets of possible concrete types for each intersect.
*
* This is often used to determine if a fragment of a given type could possibly
* be visited in a context of another type.
*
* @param Schema $schema
* @param TypeInterface $typeA
* @param TypeInterface $typeB
* @return bool
*
* @throws InvariantException
*/
public static function doTypesOverlap(Schema $schema, TypeInterface $typeA, TypeInterface $typeB): bool
{
// Equivalent types overlap
if ($typeA === $typeB) {
return true;
}
if ($typeA instanceof AbstractTypeInterface) {
if ($typeB instanceof AbstractTypeInterface) {
// If both types are abstract, then determine if there is any intersection
// between possible concrete types of each.
return arraySome($schema->getPossibleTypes($typeA),
function (NamedTypeInterface $type) use ($schema, $typeB) {
return $schema->isPossibleType($typeB, $type);
});
}
// Determine if the latter type is a possible concrete type of the former.
/** @noinspection PhpParamsInspection */
return $schema->isPossibleType($typeA, $typeB);
}
if ($typeB instanceof AbstractTypeInterface) {
// Determine if the former type is a possible concrete type of the latter.
/** @noinspection PhpParamsInspection */
return $schema->isPossibleType($typeB, $typeA);
}
// Otherwise the types do not overlap.
return false;
}
/**
* 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.
*
* @param TypeInterface $typeA
* @param TypeInterface $typeB
* @return bool
*/
public static function compareTypes(TypeInterface $typeA, TypeInterface $typeB): bool
{
if ($typeA instanceof ListType) {
return $typeB instanceof ListType
? self::compareTypes($typeA->getOfType(), $typeB->getOfType())
: true;
}
if ($typeB instanceof ListType) {
return true;
}
if ($typeA instanceof NonNullType) {
return $typeB instanceof NonNullType
? self::compareTypes($typeA->getOfType(), $typeB->getOfType())
: true;
}
if ($typeB instanceof NonNullType) {
return true;
}
if ($typeA instanceof LeafTypeInterface || $typeB instanceof LeafTypeInterface) {
return $typeA !== $typeB;
}
return false;
}
}
================================================
FILE: src/Util/TypeInfo.php
================================================
schema = $schema;
$this->getFieldDefinitionFunction = $getFieldDefinitionFunction ?? function (
Schema $schema,
TypeInterface $parentType,
FieldNode $fieldNode
) {
return getFieldDefinition($schema, $parentType, $fieldNode);
};
if ($initialType instanceof InputTypeInterface) {
$this->inputTypeStack[] = $initialType;
} elseif ($initialType instanceof CompositeTypeInterface) {
$this->parentTypeStack[] = $initialType;
} elseif ($initialType instanceof OutputTypeInterface) {
$this->typeStack[] = $initialType;
}
}
/**
* @param Schema $schema
* @param TypeInterface $parentType
* @param FieldNode $fieldNode
* @return Field|null
*/
public function resolveFieldDefinition(
Schema $schema,
TypeInterface $parentType,
FieldNode $fieldNode
): ?Field {
return \call_user_func($this->getFieldDefinitionFunction, $schema, $parentType, $fieldNode);
}
/**
* @param TypeInterface|null $type
*/
public function pushType(?TypeInterface $type): void
{
$this->typeStack[] = $type;
}
/**
*
*/
public function popType(): void
{
\array_pop($this->typeStack);
}
/**
* @return TypeInterface|TypeInterface|null
*/
public function getType(): ?TypeInterface
{
return $this->getFromStack($this->typeStack, 1);
}
/**
* @param CompositeTypeInterface|null $type
*/
public function pushParentType(?CompositeTypeInterface $type): void
{
$this->parentTypeStack[] = $type;
}
/**
*
*/
public function popParentType(): void
{
\array_pop($this->parentTypeStack);
}
/**
* @return CompositeTypeInterface|null
*/
public function getParentType(): ?CompositeTypeInterface
{
return $this->getFromStack($this->parentTypeStack, 1);
}
/**
* @param TypeInterface|null $type
*/
public function pushInputType(?TypeInterface $type): void
{
$this->inputTypeStack[] = $type;
}
/**
*
*/
public function popInputType(): void
{
\array_pop($this->inputTypeStack);
}
/**
* @return TypeInterface|null
*/
public function getInputType(): ?TypeInterface
{
return $this->getFromStack($this->inputTypeStack, 1);
}
/**
* @return TypeInterface|null
*/
public function getParentInputType(): ?TypeInterface
{
return $this->getFromStack($this->inputTypeStack, 2);
}
/**
* @param Field|null $fieldDefinition
*/
public function pushFieldDefinition(?Field $fieldDefinition): void
{
$this->fieldDefinitionStack[] = $fieldDefinition;
}
/**
*
*/
public function popFieldDefinition(): void
{
\array_pop($this->fieldDefinitionStack);
}
/**
* @return Field|null
*/
public function getFieldDefinition(): ?Field
{
return $this->getFromStack($this->fieldDefinitionStack, 1);
}
/**
* @return Schema
*/
public function getSchema(): Schema
{
return $this->schema;
}
/**
* @param mixed|null $defaultValue
*/
public function pushDefaultValue($defaultValue): void
{
$this->defaultValueStack[] = $defaultValue;
}
/**
*
*/
public function popDefaultValue(): void
{
\array_pop($this->defaultValueStack);
}
/**
* @return mixed|null
*/
public function getDefaultValue()
{
return $this->getFromStack($this->defaultValueStack, 1);
}
/**
* @return Directive|null
*/
public function getDirective(): ?Directive
{
return $this->directive;
}
/**
* @param Directive|null $directive
*/
public function setDirective(?Directive $directive): void
{
$this->directive = $directive;
}
/**
* @return Argument|null
*/
public function getArgument(): ?Argument
{
return $this->argument;
}
/**
* @param Argument|null $argument
*/
public function setArgument(?Argument $argument): void
{
$this->argument = $argument;
}
/**
* @return EnumValue|null
*/
public function getEnumValue(): ?EnumValue
{
return $this->enumValue;
}
/**
* @param EnumValue|null $enumValue
*/
public function setEnumValue(?EnumValue $enumValue): void
{
$this->enumValue = $enumValue;
}
/**
* @param array $stack
* @param int $depth
* @return mixed|null
*/
protected function getFromStack(array $stack, int $depth)
{
$count = \count($stack);
return $count >= $depth ? $stack[$count - $depth] : null;
}
}
/**
* @param Schema $schema
* @param TypeInterface $parentType
* @param FieldNode $fieldNode
* @return Field|null
* @throws InvariantException
*/
function getFieldDefinition(Schema $schema, TypeInterface $parentType, FieldNode $fieldNode): ?Field
{
$name = $fieldNode->getNameValue();
$schemaDefinition = SchemaMetaFieldDefinition();
if ($name === $schemaDefinition->getName() && $schema->getQueryType() === $parentType) {
return $schemaDefinition;
}
$typeDefinition = TypeMetaFieldDefinition();
if ($name === $typeDefinition->getName() && $schema->getQueryType() === $parentType) {
return $typeDefinition;
}
$typeNameDefinition = TypeNameMetaFieldDefinition();
if ($name === $typeNameDefinition->getName() && $parentType instanceof CompositeTypeInterface) {
return $typeNameDefinition;
}
if ($parentType instanceof ObjectType || $parentType instanceof InterfaceType) {
$fields = $parentType->getFields();
if (isset($fields[$name])) {
return $fields[$name];
}
}
return null;
}
================================================
FILE: src/Util/ValueASTConverter.php
================================================
getOfType(), $variables);
}
/**
* @param VariableNode $node
* @param TypeInterface $type
* @param array $variables
* @return mixed
* @throws ConversionException
*/
protected static function convertVariable(VariableNode $node, TypeInterface $type, array $variables)
{
$variableName = $node->getNameValue();
if (!isset($variables[$variableName])) {
throw new ConversionException(
\sprintf('Cannot convert value for missing variable "%s".', $variableName)
);
}
// 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];
if (null === $variableValue && $type instanceof NonNullType) {
throw new ConversionException(
\sprintf('Cannot convert invalid value "%s" for variable "%s".', $variableValue, $variableName)
);
}
return $variableValue;
}
/**
* @param NodeInterface|ValueNodeInterface $node
* @param ListType $type
* @param array $variables
* @return array|null
* @throws ConversionException
* @throws InvalidTypeException
* @throws InvariantException
*/
protected static function convertListType(NodeInterface $node, ListType $type, array $variables): ?array
{
$itemType = $type->getOfType();
if ($node instanceof ListValueNode) {
$values = [];
foreach ($node->getValues() as $value) {
if (self::isMissingVariable($value, $variables)) {
if ($itemType instanceof NonNullType) {
// If an array contains a missing variable, it is either converted to
// null or if the item type is non-null, it considered invalid.
throw new ConversionException('Cannot convert value for missing non-null list item.');
}
$values[] = null;
} else {
$values[] = self::convert($value, $itemType, $variables);
}
}
return $values;
}
return [self::convert($node, $itemType, $variables)];
}
/**
* @param NodeInterface|ValueNodeInterface $node
* @param InputObjectType $type
* @param array $variables
* @return array|null
* @throws InvalidTypeException
* @throws InvariantException
* @throws ConversionException
*/
protected static function convertInputObjectType(
NodeInterface $node,
InputObjectType $type,
array $variables
): ?array {
if (!$node instanceof ObjectValueNode) {
throw new ConversionException('Input object values can only be converted form object value nodes.');
}
$values = [];
/** @var ObjectFieldNode[] $fieldNodes */
$fieldNodes = keyMap($node->getFields(), function (ObjectFieldNode $value) {
return $value->getNameValue();
});
foreach ($type->getFields() as $field) {
$name = $field->getName();
$fieldNode = $fieldNodes[$name] ?? null;
if (null === $fieldNode || self::isMissingVariable($fieldNode->getValue(), $variables)) {
if (null !== $field->getDefaultValue()) {
$values[$name] = $field->getDefaultValue();
} elseif ($field->getType() instanceof NonNullType) {
throw new ConversionException('Cannot convert input object value for missing non-null field.');
}
continue;
}
$fieldValue = self::convert($fieldNode->getValue(), $field->getType(), $variables);
$values[$name] = $fieldValue;
}
return $values;
}
/**
* @param NodeInterface|ValueNodeInterface $node
* @param EnumType $type
* @return mixed|null
* @throws InvariantException
* @throws ConversionException
*/
protected static function convertEnumType(NodeInterface $node, EnumType $type)
{
if (!$node instanceof EnumValueNode) {
throw new ConversionException('Enum values can only be converted from enum value nodes.');
}
$name = $node->getValue();
$enumValue = $type->getValue($name);
if (null === $enumValue) {
throw new ConversionException(\sprintf('Cannot convert enum value for missing value "%s".', $name));
}
return $enumValue->getValue();
}
/**
* @param NodeInterface|ValueNodeInterface $node
* @param ScalarType $type
* @param array $variables
* @return mixed|null
* @throws ConversionException
*/
protected static function convertScalarType(NodeInterface $node, ScalarType $type, array $variables)
{
// Scalars fulfill parsing a literal value via parseLiteral().
// Invalid values represent a failure to parse correctly, in which case
// no value is returned.
try {
$result = $type->parseLiteral($node, $variables);
} catch (\Exception $e) {
// This will be handled by the next if-statement.
}
if (!isset($result)) {
throw new ConversionException(\sprintf('Failed to parse literal for scalar type "%s".', (string)$type));
}
return $result;
}
/**
* @param ValueNodeInterface $node
* @param array $variables
* @return bool
*/
protected static function isMissingVariable(ValueNodeInterface $node, array $variables): bool
{
return $node instanceof VariableNode && !isset($variables[$node->getNameValue()]);
}
}
================================================
FILE: src/Util/ValueConverter.php
================================================
getOfType());
return null !== $node && $node->getKind() === NodeKindEnum::NULL ? null : $node;
}
if (null === $value) {
return new NullValueNode(null);
}
// Convert PHP array to GraphQL list. If the GraphQLType is a list, but
// the value is not an array, convert the value using the list's item type.
if ($type instanceof ListType) {
return self::convertListType($value, $type);
}
// Populate the fields of the input object by creating ASTs from each value
// in the PHP object according to the fields in the input type.
if ($type instanceof InputObjectType) {
return self::convertInputObjectType($value, $type);
}
if ($type instanceof ScalarType || $type instanceof EnumType) {
return self::convertScalarOrEnum($value, $type);
}
throw new ConversionException(\sprintf('Unknown type: %s.', (string)$type));
}
/**
* @param mixed $value
* @param ListType $type
* @return ValueNodeInterface|null
* @throws InvariantException
* @throws SyntaxErrorException
* @throws ConversionException
*/
protected static function convertListType($value, ListType $type): ?ValueNodeInterface
{
$itemType = $type->getOfType();
if (!\is_iterable($value)) {
return self::convert($value, $itemType);
}
$nodes = [];
foreach ($value as $item) {
$itemNode = self::convert($item, $itemType);
if (null !== $itemNode) {
$nodes[] = $itemNode;
}
}
return new ListValueNode($nodes, null);
}
/**
* @param mixed $value
* @param InputObjectType $type
* @return ObjectValueNode|null
* @throws InvariantException
* @throws SyntaxErrorException
* @throws ConversionException
*/
protected static function convertInputObjectType($value, InputObjectType $type): ?ObjectValueNode
{
if (null === $value || !\is_object($value)) {
return null;
}
/** @var InputField[] $fields */
$fields = \array_values($type->getFields());
$nodes = [];
foreach ($fields as $field) {
$fieldName = $field->getName();
$fieldValue = self::convert($value[$fieldName], $field->getType());
if (null !== $fieldValue) {
$nodes[] = new ObjectFieldNode(new NameNode($fieldName, null), $fieldValue, null);
}
}
return new ObjectValueNode($nodes, null);
}
/**
* @param mixed $value
* @param SerializableTypeInterface $type
* @return ValueNodeInterface|null
* @throws ConversionException
*/
protected static function convertScalarOrEnum($value, SerializableTypeInterface $type): ?ValueNodeInterface
{
// Since value is an internally represented value, it must be serialized
// to an externally represented value before converting into an AST.
$serialized = $type->serialize($value);
if (null === $serialized) {
return null;
}
// Others serialize based on their corresponding JavaScript scalar types.
if (\is_bool($serialized)) {
return new BooleanValueNode($serialized, null);
}
if (\is_float($serialized)) {
return new FloatValueNode($serialized, null);
}
if (\is_int($serialized)) {
return new IntValueNode($serialized, null);
}
if (\is_string($serialized)) {
// Enum types use Enum literals.
if ($type instanceof EnumType) {
return new EnumValueNode($serialized, null);
}
// ID types can use Int literals.
if (idType() === $type && \is_numeric($serialized)) {
return new IntValueNode($serialized, null);
}
return new StringValueNode($serialized, false, null);
}
throw new ConversionException(\sprintf('Cannot convert value to AST: %s', $serialized));
}
}
================================================
FILE: src/Util/ValueHelper.php
================================================
getNameValue() === $argumentA->getNameValue();
});
if (null === $argumentB) {
return false;
}
return printNode($argumentA->getValue()) === printNode($argumentB->getValue());
});
}
}
================================================
FILE: src/Util/utils.php
================================================
0 && $index < ($count - 1) ? ', ' : '') . ($index === ($count - 1) ? ' or ' : '') .
$item;
$index++;
return $list;
}, '');
}
/**
* Given an invalid input string and a list of valid options, returns a filtered
* list of valid options sorted based on their similarity with the input.
*
* @param string $input
* @param array $options
* @return array
*/
function suggestionList(string $input, array $options): array
{
$optionsByDistance = [];
$oLength = \count($options);
$inputThreshold = \strlen($input) / 2;
/** @noinspection ForeachInvariantsInspection */
for ($i = 0; $i < $oLength; $i++) {
// Comparison must be case-insensitive.
$distance = \levenshtein(\strtolower($input), \strtolower($options[$i]));
$threshold = \max($inputThreshold, \strlen($options[$i]) / 2, 1);
if ($distance <= $threshold) {
$optionsByDistance[$options[$i]] = $distance;
}
}
$result = \array_keys($optionsByDistance);
\usort($result, function ($a, $b) use ($optionsByDistance) {
return $optionsByDistance[$a] - $optionsByDistance[$b];
});
return $result;
}
/**
* Given `[A, B, C]` returns `'"A", "B" or "C"'`.
*
* @param array $items
* @return string
*/
function quotedOrList(array $items): string
{
return orList(\array_map(function ($item) {
return '"' . $item . '"';
}, $items));
}
/**
* @param array $array
* @param callable $fn
* @return bool
*/
function arrayEvery(array $array, callable $fn): bool
{
return \array_reduce($array, function ($result, $value) use ($fn) {
return $result && $fn($value);
}, true);
}
/**
* @param array $array
* @param callable $fn
* @return mixed
*/
function arraySome(array $array, callable $fn)
{
return \array_reduce($array, function ($result, $value) use ($fn) {
return $result || $fn($value);
});
}
/**
* @param array $array
* @param callable $predicate
* @return mixed|null
*/
function find(array $array, callable $predicate)
{
foreach ($array as $value) {
if ($predicate($value)) {
return $value;
}
}
return null;
}
/**
* @param array $array
* @param callable $keyFn
* @return array
*/
function keyMap(array $array, callable $keyFn): array
{
return \array_reduce($array, function ($map, $item) use ($keyFn) {
$map[$keyFn($item)] = $item;
return $map;
}, []);
}
/**
* @param array $array
* @param callable $keyFn
* @param callable $valFn
* @return array
*/
function keyValueMap(array $array, callable $keyFn, callable $valFn): array
{
return \array_reduce($array, function ($map, $item) use ($keyFn, $valFn) {
$map[$keyFn($item)] = $valFn($item);
return $map;
}, []);
}
/**
* @param array $map
* @return PromiseInterface
*/
function promiseForMap(array $map): PromiseInterface
{
$keys = \array_keys($map);
$promisesOrValues = \array_map(function ($name) use ($map) {
return $map[$name];
}, $keys);
return \React\Promise\all($promisesOrValues)->then(function ($values) use ($keys) {
$i = 0;
return \array_reduce($values, function ($resolvedObject, $value) use ($keys, &$i) {
$resolvedObject[$keys[$i++]] = $value;
return $resolvedObject;
}, []);
});
}
/**
* @param array $values
* @param callable $fn
* @param mixed $initial
* @return mixed
*/
function promiseReduce(array $values, callable $fn, $initial = null)
{
return \array_reduce($values, function ($previous, $value) use ($fn) {
return $previous instanceof PromiseInterface
? $previous->then(function ($resolvedValue) use ($fn, $value) {
return $fn($resolvedValue, $value);
})
: $fn($previous, $value);
}, $initial);
}
/**
* @param mixed $value
* @return string
*/
function toString($value): string
{
if ($value instanceof TypeInterface) {
return (string)$value;
}
if ($value instanceof NodeInterface) {
return printNode($value);
}
if (\is_object($value)) {
return 'Object';
}
if (\is_array($value)) {
return 'Array';
}
if (\is_callable($value)) {
return 'Function';
}
if ($value === '') {
return '(empty string)';
}
if ($value === null) {
return 'null';
}
if ($value === true) {
return 'true';
}
if ($value === false) {
return 'false';
}
if (\is_string($value)) {
return "\"{$value}\"";
}
if (\is_scalar($value)) {
return (string)$value;
}
return \gettype($value);
}
================================================
FILE: src/Validation/Conflict/ComparisonContext.php
================================================
getNode()->getAliasOrNameValue();
if (!isset($this->fieldMap[$responseName])) {
$this->fieldMap[$responseName] = [];
}
$this->fieldMap[$responseName][] = $field;
return $this;
}
/**
* @param NodeInterface|FragmentSpreadNode|FragmentDefinitionNode $fragment
* @return $this
*/
public function registerFragment(NodeInterface $fragment)
{
if ($fragment instanceof NameAwareInterface) {
$this->fragmentNames[] = $fragment->getNameValue();
}
return $this;
}
/**
* @param Conflict $conflict
* @return $this
*/
public function reportConflict(Conflict $conflict)
{
$this->conflicts[] = $conflict;
return $this;
}
/**
* @return array
*/
public function getFieldMap(): array
{
return $this->fieldMap;
}
/**
* @return array
*/
public function getFragmentNames(): array
{
return $this->fragmentNames;
}
/**
* @return bool
*/
public function hasConflicts(): bool
{
return !empty($this->conflicts);
}
/**
* @return Conflict[]
*/
public function getConflicts(): array
{
return $this->conflicts;
}
}
================================================
FILE: src/Validation/Conflict/Conflict.php
================================================
responseName = $responseName;
$this->reason = $reason;
$this->fieldsA = $fieldsA;
$this->fieldsB = $fieldsB;
}
/**
* @return string
*/
public function getResponseName(): string
{
return $this->responseName;
}
/**
* @return mixed
*/
public function getReason()
{
return $this->reason;
}
/**
* @return array
*/
public function getFieldsA(): array
{
return $this->fieldsA;
}
/**
* @return array
*/
public function getFieldsB(): array
{
return $this->fieldsB;
}
/**
* @return array
*/
public function getAllFields(): array
{
return \array_merge($this->fieldsA, $this->fieldsB);
}
}
================================================
FILE: src/Validation/Conflict/ConflictFinder.php
================================================
cachedFieldsAndFragmentNames = new \SplObjectStorage();
$this->comparedFragmentPairs = new PairSet();
}
/**
* @param SelectionSetNode $selectionSet
* @param NamedTypeInterface|null $parentType
* @return array|Conflict[]
* @throws InvalidTypeException
* @throws InvariantException
* @throws ConversionException
*/
public function findConflictsWithinSelectionSet(
SelectionSetNode $selectionSet,
?NamedTypeInterface $parentType = null
): array {
$context = $this->getFieldsAndFragmentNames($selectionSet, $parentType);
// (A) Find find all conflicts "within" the fields of this selection set.
// Note: this is the *only place* `collectConflictsWithin` is called.
$this->collectConflictsWithin($context);
$fieldMap = $context->getFieldMap();
$fragmentNames = $context->getFragmentNames();
// (B) Then collect conflicts between these fields and those represented by
// each spread fragment name found.
if (!empty($fragmentNames)) {
$fragmentNamesCount = \count($fragmentNames);
$comparedFragments = [];
/** @noinspection ForeachInvariantsInspection */
for ($i = 0; $i < $fragmentNamesCount; $i++) {
$this->collectConflictsBetweenFieldsAndFragment(
$context,
$comparedFragments,
$fieldMap,
$fragmentNames[$i],
false/* $areMutuallyExclusive */
);
// (C) Then compare this fragment with all other fragments found in this
// selection set to collect conflicts between fragments spread together.
// This compares each item in the list of fragment names to every other
// item in that same list (except for itself).
for ($j = $i + 1; $j < $fragmentNamesCount; $j++) {
$this->collectConflictsBetweenFragments(
$context,
$fragmentNames[$i],
$fragmentNames[$j],
false/* $areMutuallyExclusive */
);
}
}
}
return $context->getConflicts();
}
/**
* Collect all conflicts found between a set of fields and a fragment reference
* including via spreading in any nested fragments.
*
* @param ComparisonContext $context
* @param array $comparedFragments
* @param array $fieldMap
* @param string $fragmentName
* @param bool $areMutuallyExclusive
* @throws ConversionException
* @throws InvalidTypeException
* @throws InvariantException
*/
protected function collectConflictsBetweenFieldsAndFragment(
ComparisonContext $context,
array &$comparedFragments,
array $fieldMap,
string $fragmentName,
bool $areMutuallyExclusive
): void {
// Memoize so a fragment is not compared for conflicts more than once.
if (isset($comparedFragments[$fragmentName])) {
return;
}
$comparedFragments[$fragmentName] = true;
$fragment = $this->getContext()->getFragment($fragmentName);
if (null === $fragment) {
return;
}
$contextB = $this->getReferencedFieldsAndFragmentNames($fragment);
$fieldMapB = $contextB->getFieldMap();
// Do not compare a fragment's fieldMap to itself.
if ($fieldMap == $fieldMapB) {
return;
}
// (D) First collect any conflicts between the provided collection of fields
// and the collection of fields represented by the given fragment.
$this->collectConflictsBetween(
$context,
$fieldMap,
$fieldMapB,
$areMutuallyExclusive
);
$fragmentNamesB = $contextB->getFragmentNames();
// (E) Then collect any conflicts between the provided collection of fields
// and any fragment names found in the given fragment.
if (!empty($fragmentNamesB)) {
$fragmentNamesBCount = \count($fragmentNamesB);
/** @noinspection ForeachInvariantsInspection */
for ($i = 0; $i < $fragmentNamesBCount; $i++) {
$this->collectConflictsBetweenFieldsAndFragment(
$context,
$comparedFragments,
$fieldMap,
$fragmentNamesB[$i],
$areMutuallyExclusive
);
}
}
}
/**
* Collect all conflicts found between two fragments, including via spreading in
* any nested fragments.
*
* @param ComparisonContext $context
* @param string $fragmentNameA
* @param string $fragmentNameB
* @param bool $areMutuallyExclusive
* @throws ConversionException
* @throws InvalidTypeException
* @throws InvariantException
*/
protected function collectConflictsBetweenFragments(
ComparisonContext $context,
string $fragmentNameA,
string $fragmentNameB,
bool $areMutuallyExclusive
): void {
// No need to compare a fragment to itself.
if ($fragmentNameA === $fragmentNameB) {
return;
}
// Memoize so two fragments are not compared for conflicts more than once.
if ($this->comparedFragmentPairs->has($fragmentNameA, $fragmentNameB, $areMutuallyExclusive)) {
return;
}
$this->comparedFragmentPairs->add($fragmentNameA, $fragmentNameB, $areMutuallyExclusive);
$fragmentA = $this->getContext()->getFragment($fragmentNameA);
$fragmentB = $this->getContext()->getFragment($fragmentNameB);
if (null === $fragmentA || null === $fragmentB) {
return;
}
$contextA = $this->getReferencedFieldsAndFragmentNames($fragmentA);
$contextB = $this->getReferencedFieldsAndFragmentNames($fragmentB);
// (F) First, collect all conflicts between these two collections of fields
// (not including any nested fragments).
$this->collectConflictsBetween(
$context,
$contextA->getFieldMap(),
$contextB->getFieldMap(),
$areMutuallyExclusive
);
$fragmentNamesB = $contextB->getFragmentNames();
// (G) Then collect conflicts between the first fragment and any nested
// fragments spread in the second fragment.
if (!empty($fragmentNamesB)) {
$fragmentNamesBCount = \count($fragmentNamesB);
/** @noinspection ForeachInvariantsInspection */
for ($j = 0; $j < $fragmentNamesBCount; $j++) {
$this->collectConflictsBetweenFragments(
$context,
$fragmentNameA,
$fragmentNamesB[$j],
$areMutuallyExclusive
);
}
}
$fragmentNamesA = $contextA->getFragmentNames();
// (G) Then collect conflicts between the second fragment and any nested
// fragments spread in the first fragment.
if (!empty($fragmentNamesA)) {
$fragmentNamesACount = \count($fragmentNamesA);
/** @noinspection ForeachInvariantsInspection */
for ($i = 0; $i < $fragmentNamesACount; $i++) {
$this->collectConflictsBetweenFragments(
$context,
$fragmentNamesA[$i],
$fragmentNameB,
$areMutuallyExclusive
);
}
}
}
/**
* Find all conflicts found between two selection sets, including those found
* via spreading in fragments. Called when determining if conflicts exist
* between the sub-fields of two overlapping fields.
*
* @param NamedTypeInterface|null $parentTypeA
* @param SelectionSetNode $selectionSetA
* @param NamedTypeInterface|null $parentTypeB
* @param SelectionSetNode $selectionSetB
* @param bool $areMutuallyExclusive
* @return Conflict[]
* @throws InvalidTypeException
* @throws InvariantException
* @throws ConversionException
*/
protected function findConflictsBetweenSubSelectionSets(
?NamedTypeInterface $parentTypeA,
SelectionSetNode $selectionSetA,
?NamedTypeInterface $parentTypeB,
SelectionSetNode $selectionSetB,
bool $areMutuallyExclusive
): array {
$context = new ComparisonContext();
$contextA = $this->getFieldsAndFragmentNames($selectionSetA, $parentTypeA);
$contextB = $this->getFieldsAndFragmentNames($selectionSetB, $parentTypeB);
$fieldMapA = $contextA->getFieldMap();
$fieldMapB = $contextB->getFieldMap();
$fragmentNamesA = $contextA->getFragmentNames();
$fragmentNamesB = $contextB->getFragmentNames();
$fragmentNamesACount = \count($fragmentNamesA);
$fragmentNamesBCount = \count($fragmentNamesB);
// (H) First, collect all conflicts between these two collections of field.
$this->collectConflictsBetween(
$context,
$fieldMapA,
$fieldMapB,
$areMutuallyExclusive
);
// (I) Then collect conflicts between the first collection of fields and
// those referenced by each fragment name associated with the second.
if (!empty($fragmentNamesB)) {
$comparedFragments = [];
/** @noinspection ForeachInvariantsInspection */
for ($j = 0; $j < $fragmentNamesBCount; $j++) {
$this->collectConflictsBetweenFieldsAndFragment(
$context,
$comparedFragments,
$fieldMapA,
$fragmentNamesB[$j],
$areMutuallyExclusive
);
}
}
// (I) Then collect conflicts between the second collection of fields and
// those referenced by each fragment name associated with the first.
if (!empty($fragmentNamesA)) {
$comparedFragments = [];
/** @noinspection ForeachInvariantsInspection */
for ($i = 0; $i < $fragmentNamesACount; $i++) {
$this->collectConflictsBetweenFieldsAndFragment(
$context,
$comparedFragments,
$fieldMapB,
$fragmentNamesA[$i],
$areMutuallyExclusive
);
}
}
/** @noinspection ForeachInvariantsInspection */
for ($i = 0; $i < $fragmentNamesACount; $i++) {
/** @noinspection ForeachInvariantsInspection */
for ($j = 0; $j < $fragmentNamesBCount; $j++) {
$this->collectConflictsBetweenFragments(
$context,
$fragmentNamesA[$i],
$fragmentNamesB[$j],
$areMutuallyExclusive
);
}
}
return $context->getConflicts();
}
/**
* Collect all Conflicts "within" one collection of fields.
*
* @param ComparisonContext $context
* @throws ConversionException
* @throws InvalidTypeException
* @throws InvariantException
*/
protected function collectConflictsWithin(ComparisonContext $context): void
{
// A field map is a keyed collection, where each key represents a response
// name and the value at that key is a list of all fields which provide that
// response name. For every response name, if there are multiple fields, they
// must be compared to find a potential conflict.
foreach ($context->getFieldMap() as $responseName => $fields) {
$fieldsCount = \count($fields);
// This compares every field in the list to every other field in this list
// (except to itself). If the list only has one item, nothing needs to
// be compared.
if ($fieldsCount > 1) {
/** @noinspection ForeachInvariantsInspection */
for ($i = 0; $i < $fieldsCount; $i++) {
for ($j = $i + 1; $j < $fieldsCount; $j++) {
$conflict = $this->findConflict(
$responseName,
$fields[$i],
$fields[$j],
// within one collection is never mutually exclusive
false/* $areMutuallyExclusive */
);
if (null !== $conflict) {
$context->reportConflict($conflict);
}
}
}
}
}
}
/**
* Collect all Conflicts between two collections of fields. This is similar to,
* but different from the `collectConflictsWithin` function above. This check
* assumes that `collectConflictsWithin` has already been called on each
* provided collection of fields. This is true because this validator traverses
* each individual selection set.
*
* @param ComparisonContext $context
* @param array $fieldMapA
* @param array $fieldMapB
* @param bool $parentFieldsAreMutuallyExclusive
* @throws ConversionException
* @throws InvalidTypeException
* @throws InvariantException
*/
protected function collectConflictsBetween(
ComparisonContext $context,
array $fieldMapA,
array $fieldMapB,
bool $parentFieldsAreMutuallyExclusive
): void {
// A field map is a keyed collection, where each key represents a response
// name and the value at that key is a list of all fields which provide that
// response name. For any response name which appears in both provided field
// maps, each field from the first field map must be compared to every field
// in the second field map to find potential conflicts.
foreach ($fieldMapA as $responseName => $fieldsA) {
$fieldsB = $fieldMapB[$responseName] ?? null;
if (null !== $fieldsB) {
$fieldsACount = \count($fieldsA);
$fieldsBCount = \count($fieldsB);
/** @noinspection ForeachInvariantsInspection */
for ($i = 0; $i < $fieldsACount; $i++) {
/** @noinspection ForeachInvariantsInspection */
for ($j = 0; $j < $fieldsBCount; $j++) {
$conflict = $this->findConflict(
$responseName,
$fieldsA[$i],
$fieldsB[$j],
$parentFieldsAreMutuallyExclusive
);
if (null !== $conflict) {
$context->reportConflict($conflict);
}
}
}
}
}
}
/**
* Determines if there is a conflict between two particular fields, including
* comparing their sub-fields.
*
* @param string $responseName
* @param FieldContext $fieldA
* @param FieldContext $fieldB
* @param bool $parentFieldsAreMutuallyExclusive
* @return Conflict|null
* @throws ConversionException
* @throws InvalidTypeException
* @throws InvariantException
*/
protected function findConflict(
string $responseName,
FieldContext $fieldA,
FieldContext $fieldB,
bool $parentFieldsAreMutuallyExclusive
): ?Conflict {
$parentTypeA = $fieldA->getParentType();
$parentTypeB = $fieldB->getParentType();
// If it is known that two fields could not possibly apply at the same
// time, due to the parent types, then it is safe to permit them to diverge
// in aliased field or arguments used as they will not present any ambiguity
// by differing.
// It is known that two parent types could never overlap if they are
// different Object types. Interface or Union types might overlap - if not
// in the current state of the schema, then perhaps in some future version,
// thus may not safely diverge.
$areMutuallyExclusive = $parentFieldsAreMutuallyExclusive
|| ($parentTypeA !== $parentTypeB
&& $parentTypeA instanceof ObjectType
&& $parentTypeB instanceof ObjectType);
$nodeA = $fieldA->getNode();
$nodeB = $fieldB->getNode();
$definitionA = $fieldA->getDefinition();
$definitionB = $fieldB->getDefinition();
if (!$areMutuallyExclusive) {
// Two aliases must refer to the same field.
$nameA = $nodeA->getNameValue();
$nameB = $nodeB->getNameValue();
if ($nameA !== $nameB) {
return new Conflict(
$responseName,
sprintf('%s and %s are different fields', $nameA, $nameB),
[$nodeA],
[$nodeB]
);
}
// Two field calls must have the same arguments.
if (!ValueHelper::compareArguments($nodeA->getArguments(), $nodeB->getArguments())) {
return new Conflict(
$responseName,
'they have differing arguments',
[$nodeA],
[$nodeB]
);
}
}
// The return type for each field.
$typeA = null !== $definitionA ? $definitionA->getType() : null;
$typeB = null !== $definitionB ? $definitionB->getType() : null;
if (null !== $typeA && null !== $typeB && TypeHelper::compareTypes($typeA, $typeB)) {
return new Conflict(
$responseName,
sprintf('they return conflicting types %s and %s', (string)$typeA, (string)$typeB),
[$nodeA],
[$nodeB]
);
}
// Collect and compare sub-fields. Use the same "visited fragment names" list
// for both collections so fields in a fragment reference are never
// compared to themselves.
$selectionSetA = $nodeA->getSelectionSet();
$selectionSetB = $nodeB->getSelectionSet();
if (null !== $selectionSetA && null !== $selectionSetB) {
$conflicts = $this->findConflictsBetweenSubSelectionSets(
getNamedType($typeA),
$selectionSetA,
getNamedType($typeB),
$selectionSetB,
$areMutuallyExclusive
);
return $this->subfieldConflicts($conflicts, $responseName, $nodeA, $nodeB);
}
return null;
}
/**
* Given a selection set, return the collection of fields (a mapping of response
* name to field nodes and definitions) as well as a list of fragment names
* referenced via fragment spreads.
*
* @param SelectionSetNode $selectionSet
* @param NamedTypeInterface|null $parentType
* @return ComparisonContext
* @throws InvalidTypeException
* @throws InvariantException
* @throws ConversionException
*/
protected function getFieldsAndFragmentNames(
SelectionSetNode $selectionSet,
?NamedTypeInterface $parentType
): ComparisonContext {
if (!$this->cachedFieldsAndFragmentNames->offsetExists($selectionSet)) {
$cached = new ComparisonContext();
$this->collectFieldsAndFragmentNames($cached, $selectionSet, $parentType);
$this->cachedFieldsAndFragmentNames->offsetSet($selectionSet, $cached);
}
return $this->cachedFieldsAndFragmentNames->offsetGet($selectionSet);
}
/**
* Given a reference to a fragment, return the represented collection of fields
* as well as a list of nested fragment names referenced via fragment spreads.
*
* @param FragmentDefinitionNode $fragment
* @return ComparisonContext
* @throws InvalidTypeException
* @throws ConversionException
* @throws InvariantException
*/
protected function getReferencedFieldsAndFragmentNames(FragmentDefinitionNode $fragment): ComparisonContext
{
if ($this->cachedFieldsAndFragmentNames->offsetExists($fragment)) {
return $this->cachedFieldsAndFragmentNames->offsetGet($fragment);
}
/** @var NamedTypeInterface $fragmentType */
$fragmentType = TypeASTConverter::convert($this->getContext()->getSchema(), $fragment->getTypeCondition());
return $this->getFieldsAndFragmentNames($fragment->getSelectionSet(), $fragmentType);
}
/**
* @param ComparisonContext $context
* @param SelectionSetNode $selectionSet
* @param NamedTypeInterface|null $parentType
* @throws InvalidTypeException
* @throws InvariantException
* @throws ConversionException
*/
protected function collectFieldsAndFragmentNames(
ComparisonContext $context,
SelectionSetNode $selectionSet,
?NamedTypeInterface $parentType
): void {
foreach ($selectionSet->getSelections() as $selection) {
if ($selection instanceof FieldNode) {
$definition = ($parentType instanceof ObjectType || $parentType instanceof InterfaceType)
? ($parentType->getFields()[$selection->getNameValue()] ?? null)
: null;
$context->registerField(new FieldContext($parentType, $selection, $definition));
} elseif ($selection instanceof FragmentSpreadNode) {
$context->registerFragment($selection);
} elseif ($selection instanceof InlineFragmentNode) {
$typeCondition = $selection->getTypeCondition();
$inlineFragmentType = null !== $typeCondition
? TypeASTConverter::convert($this->getContext()->getSchema(), $typeCondition)
: $parentType;
$this->collectFieldsAndFragmentNames($context, $selection->getSelectionSet(), $inlineFragmentType);
}
}
}
/**
* Given a series of Conflicts which occurred between two sub-fields, generate
* a single Conflict.
*
* @param array|Conflict[] $conflicts
* @param string $responseName
* @param FieldNode $nodeA
* @param FieldNode $nodeB
* @return Conflict|null
*/
protected function subfieldConflicts(
array $conflicts,
string $responseName,
FieldNode $nodeA,
FieldNode $nodeB
): ?Conflict {
if (empty($conflicts)) {
return null;
}
return new Conflict(
$responseName,
array_map(function (Conflict $conflict) {
return [$conflict->getResponseName(), $conflict->getReason()];
}, $conflicts),
array_reduce($conflicts, function ($allFields, Conflict $conflict) {
return array_merge($allFields, $conflict->getFieldsA());
}, [$nodeA]),
array_reduce($conflicts, function ($allFields, Conflict $conflict) {
return array_merge($allFields, $conflict->getFieldsB());
}, [$nodeB])
);
}
}
================================================
FILE: src/Validation/Conflict/FieldContext.php
================================================
parentType = $parentType;
$this->node = $node;
$this->definition = $definition;
}
/**
* @return NamedTypeInterface|null
*/
public function getParentType(): ?NamedTypeInterface
{
return $this->parentType;
}
/**
* @return FieldNode
*/
public function getNode(): FieldNode
{
return $this->node;
}
/**
* @return Field|null
*/
public function getDefinition(): ?Field
{
return $this->definition;
}
}
================================================
FILE: src/Validation/Conflict/PairSet.php
================================================
data[$a] ?? null;
$result = (null !== $first && isset($first[$b])) ? $first[$b] : null;
if (null === $result) {
return false;
}
// areMutuallyExclusive being false is a superset of being true,
// hence if we want to know if this PairSet "has" these two with no
// exclusivity, we have to ensure it was added as such.
if ($areMutuallyExclusive === false) {
return $result === false;
}
return true;
}
/**
* @param string $a
* @param string $b
* @param bool $areMutuallyExclusive
*/
public function add(string $a, string $b, bool $areMutuallyExclusive): void
{
$this->addToData($a, $b, $areMutuallyExclusive);
$this->addToData($b, $a, $areMutuallyExclusive);
}
/**
* @param string $a
* @param string $b
* @param bool $areMutuallyExclusive
*/
protected function addToData(string $a, string $b, bool $areMutuallyExclusive): void
{
$map = $this->data[$a] ?? [];
$map[$b] = $areMutuallyExclusive;
$this->data[$a] = $map;
}
}
================================================
FILE: src/Validation/Rule/AbstractRule.php
================================================
getDefinitions() as $definition) {
if (!$definition instanceof ExecutableDefinitionNodeInterface) {
$this->context->reportError(
new ValidationException(
nonExecutableDefinitionMessage($this->getDefinitionName($definition)),
[$definition]
)
);
}
}
return new VisitorResult($node);
}
/**
* @param NodeInterface $node
* @return null|string
*/
protected function getDefinitionName(NodeInterface $node): ?string
{
if ($node instanceof SchemaDefinitionNode || $node instanceof SchemaExtensionNode) {
return 'schema';
}
return $node instanceof NameAwareInterface
? $node->getNameValue()
: null;
}
}
================================================
FILE: src/Validation/Rule/FieldOnCorrectTypeRule.php
================================================
context->getParentType();
if ($type instanceof OutputTypeInterface) {
$fieldDefinition = $this->context->getFieldDefinition();
if (null === $fieldDefinition) {
$schema = $this->context->getSchema();
$fieldName = $node->getNameValue();
$suggestedTypeNames = $this->getSuggestedTypeNames($schema, $type, $fieldName);
$suggestedFieldNames = \count($suggestedTypeNames) !== 0
? []
: $this->getSuggestedFieldNames($type, $fieldName);
$this->context->reportError(
new ValidationException(
undefinedFieldMessage(
$fieldName,
(string)$type,
$suggestedTypeNames,
$suggestedFieldNames
),
[$node]
)
);
}
}
return new VisitorResult($node);
}
/**
* Go through all of the implementations of type, as well as the interfaces
* that they implement. If any of those types include the provided field,
* suggest them, sorted by how often the type is referenced, starting
* with Interfaces.
*
* @param Schema $schema
* @param TypeInterface $type
* @param string $fieldName
* @return array
* @throws InvariantException
*/
protected function getSuggestedTypeNames(Schema $schema, TypeInterface $type, string $fieldName): array
{
if (!$type instanceof AbstractTypeInterface) {
// Otherwise, must be an Object type, which does not have possible fields.
return [];
}
$suggestedObjectTypes = [];
$interfaceUsageCount = [];
foreach ($schema->getPossibleTypes($type) as $possibleType) {
if (!$possibleType instanceof FieldsAwareInterface) {
continue;
}
$typeFields = $possibleType->getFields();
if (!isset($typeFields[$fieldName])) {
break;
}
if (!$possibleType instanceof NamedTypeInterface) {
continue;
}
$suggestedObjectTypes[] = $possibleType->getName();
if (!$possibleType instanceof ObjectType) {
continue;
}
foreach ($possibleType->getInterfaces() as $possibleInterface) {
$interfaceFields = $possibleInterface->getFields();
if (!isset($interfaceFields[$fieldName])) {
break;
}
$interfaceName = $possibleInterface->getName();
$interfaceUsageCount[$interfaceName] = ($interfaceUsageCount[$interfaceName] ?? 0) + 1;
}
}
$suggestedInterfaceTypes = \array_keys($interfaceUsageCount);
\uasort($suggestedInterfaceTypes, function ($a, $b) use ($interfaceUsageCount) {
return $interfaceUsageCount[$b] - $interfaceUsageCount[$a];
});
return \array_merge($suggestedInterfaceTypes, $suggestedObjectTypes);
}
/**
* For the field name provided, determine if there are any similar field names
* that may be the result of a typo.
*
* @param OutputTypeInterface $type
* @param string $fieldName
* @return array
* @throws \Exception
*/
protected function getSuggestedFieldNames(OutputTypeInterface $type, string $fieldName): array
{
if (!($type instanceof ObjectType || $type instanceof InterfaceType)) {
// Otherwise, must be a Union type, which does not define fields.
return [];
}
$possibleFieldNames = \array_keys($type->getFields());
return suggestionList($fieldName, $possibleFieldNames);
}
}
================================================
FILE: src/Validation/Rule/FragmentsOnCompositeTypesRule.php
================================================
validateFragementNode($node, function (FragmentDefinitionNode $node) {
return fragmentOnNonCompositeMessage((string)$node, (string)$node->getTypeCondition());
});
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function enterInlineFragment(InlineFragmentNode $node): VisitorResult
{
$this->validateFragementNode($node, function (InlineFragmentNode $node) {
return inlineFragmentOnNonCompositeMessage((string)$node->getTypeCondition());
});
return new VisitorResult($node);
}
/**
* @param InlineFragmentNode|FragmentDefinitionNode $node
* @param callable $errorMessageFunction
* @throws \Exception
* @throws \TypeError
*/
protected function validateFragementNode($node, callable $errorMessageFunction)
{
$typeCondition = $node->getTypeCondition();
if (null !== $typeCondition) {
$type = TypeASTConverter::convert($this->context->getSchema(), $typeCondition);
if (null !== $type && !($type instanceof CompositeTypeInterface)) {
$this->context->reportError(
new ValidationException($errorMessageFunction($node),
[$typeCondition]
)
);
}
}
}
}
================================================
FILE: src/Validation/Rule/KnownArgumentNamesRule.php
================================================
context->getArgument();
if (null === $argumentDefinition) {
$argumentOf = $node->getAncestor();
if ($argumentOf instanceof FieldNode) {
return new VisitorResult($this->validateField($node));
}
if ($argumentOf instanceof DirectiveNode) {
return new VisitorResult($this->validateDirective($node));
}
}
return new VisitorResult($node);
}
/**
* @param NodeInterface $node
* @return NodeInterface|null
*/
protected function validateField(NodeInterface $node): ?NodeInterface
{
$fieldDefinition = $this->context->getFieldDefinition();
$parentType = $this->context->getParentType();
if (null !== $fieldDefinition && null !== $parentType) {
/** @noinspection PhpUnhandledExceptionInspection */
$options = \array_map(function (Argument $argument) {
return $argument->getName();
}, $fieldDefinition->getArguments());
$suggestions = suggestionList(printNode($node), $options);
$this->context->reportError(
new ValidationException(
unknownArgumentMessage(
(string)$node,
(string)$fieldDefinition,
(string)$parentType,
$suggestions
),
[$node]
)
);
}
return $node;
}
/**
* @param NodeInterface $node
* @return NodeInterface|null
*/
protected function validateDirective(NodeInterface $node): ?NodeInterface
{
$directive = $this->context->getDirective();
if (null !== $directive) {
/** @noinspection PhpUnhandledExceptionInspection */
$options = \array_map(function (Argument $argument) {
return $argument->getName();
}, $directive->getArguments());
$suggestions = suggestionList((string)$node, $options);
$this->context->reportError(
new ValidationException(
unknownDirectiveArgumentMessage((string)$node, (string)$directive, $suggestions),
[$node]
)
);
}
return $node;
}
}
================================================
FILE: src/Validation/Rule/KnownDirectivesRule.php
================================================
context->getSchema()->getDirectives(),
function (Directive $definition) use ($node) {
return $definition->getName() === $node->getNameValue();
}
);
if (null == $directiveDefinition) {
$this->context->reportError(
new ValidationException(unknownDirectiveMessage((string)$node), [$node])
);
return new VisitorResult($node);
}
$location = $this->getDirectiveLocationFromASTPath($node);
if (null !== $location && !\in_array($location, $directiveDefinition->getLocations())) {
$this->context->reportError(
new ValidationException(misplacedDirectiveMessage((string)$node, $location), [$node])
);
}
return new VisitorResult($node);
}
/**
* @param NodeInterface $node
* @return string|null
*/
protected function getDirectiveLocationFromASTPath(NodeInterface $node): ?string
{
$appliedTo = $node->getAncestor();
if ($appliedTo instanceof OperationDefinitionNode) {
switch ($appliedTo->getOperation()) {
case 'query':
return DirectiveLocationEnum::QUERY;
case 'mutation':
return DirectiveLocationEnum::MUTATION;
case 'subscription':
return DirectiveLocationEnum::SUBSCRIPTION;
default:
return null;
}
}
if ($appliedTo instanceof FieldNode) {
return DirectiveLocationEnum::FIELD;
}
if ($appliedTo instanceof FragmentSpreadNode) {
return DirectiveLocationEnum::FRAGMENT_SPREAD;
}
if ($appliedTo instanceof InlineFragmentNode) {
return DirectiveLocationEnum::INLINE_FRAGMENT;
}
if ($appliedTo instanceof FragmentDefinitionNode) {
return DirectiveLocationEnum::FRAGMENT_DEFINITION;
}
if ($appliedTo instanceof SchemaDefinitionNode || $appliedTo instanceof SchemaExtensionNode) {
return DirectiveLocationEnum::SCHEMA;
}
if ($appliedTo instanceof ScalarTypeDefinitionNode || $appliedTo instanceof ScalarTypeExtensionNode) {
return DirectiveLocationEnum::SCALAR;
}
if ($appliedTo instanceof ObjectTypeDefinitionNode || $appliedTo instanceof ObjectTypeExtensionNode) {
return DirectiveLocationEnum::OBJECT;
}
if ($appliedTo instanceof FieldDefinitionNode) {
return DirectiveLocationEnum::FIELD_DEFINITION;
}
if ($appliedTo instanceof InterfaceTypeDefinitionNode || $appliedTo instanceof InterfaceTypeExtensionNode) {
return DirectiveLocationEnum::INTERFACE;
}
if ($appliedTo instanceof UnionTypeDefinitionNode || $appliedTo instanceof UnionTypeExtensionNode) {
return DirectiveLocationEnum::UNION;
}
if ($appliedTo instanceof EnumTypeDefinitionNode || $appliedTo instanceof EnumTypeExtensionNode) {
return DirectiveLocationEnum::ENUM;
}
if ($appliedTo instanceof EnumValueDefinitionNode) {
return DirectiveLocationEnum::ENUM_VALUE;
}
if ($appliedTo instanceof InputObjectTypeDefinitionNode || $appliedTo instanceof InputObjectTypeExtensionNode) {
return DirectiveLocationEnum::INPUT_OBJECT;
}
if ($appliedTo instanceof InputValueDefinitionNode) {
$parentNode = $node->getAncestor(2);
return $parentNode instanceof InputObjectTypeDefinitionNode
? DirectiveLocationEnum::INPUT_FIELD_DEFINITION
: DirectiveLocationEnum::ARGUMENT_DEFINITION;
}
return null;
}
}
================================================
FILE: src/Validation/Rule/KnownFragmentNamesRule.php
================================================
getNameValue();
$fragment = $this->context->getFragment($fragmentName);
if (null === $fragment) {
$this->context->reportError(
new ValidationException(unknownFragmentMessage($fragmentName), [$node->getName()])
);
}
return new VisitorResult($node);
}
}
================================================
FILE: src/Validation/Rule/KnownTypeNamesRule.php
================================================
context->getSchema();
$typeName = $node->getNameValue();
$type = $schema->getType($typeName);
if (null === $type) {
$this->context->reportError(
new ValidationException(
unknownTypeMessage($typeName, suggestionList($typeName, \array_keys($schema->getTypeMap()))),
[$node]
)
);
}
return new VisitorResult($node);
}
// TODO: when validating IDL, re-enable these. Experimental version does not add unreferenced types, resulting in false-positive errors. Squelched errors for now.
/**
* @inheritdoc
*/
protected function enterObjectTypeDefinition(ObjectTypeDefinitionNode $node): VisitorResult
{
return new VisitorResult(null);
}
/**
* @inheritdoc
*/
protected function enterInterfaceTypeDefinition(InterfaceTypeDefinitionNode $node): VisitorResult
{
return new VisitorResult(null);
}
/**
* @inheritdoc
*/
protected function enterUnionTypeDefinition(UnionTypeDefinitionNode $node): VisitorResult
{
return new VisitorResult(null);
}
/**
* @inheritdoc
*/
protected function enterInputObjectTypeDefinition(InputObjectTypeDefinitionNode $node): VisitorResult
{
return new VisitorResult(null);
}
}
================================================
FILE: src/Validation/Rule/LoneAnonymousOperationRule.php
================================================
operationCount = \count(\array_filter($node->getDefinitions(), function ($definition) {
return $definition instanceof OperationDefinitionNode;
}));
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function enterOperationDefinition(OperationDefinitionNode $node): VisitorResult
{
if (null === $node->getName() && $this->operationCount > 1) {
$this->context->reportError(
new ValidationException(anonymousOperationNotAloneMessage(), [$node])
);
}
return new VisitorResult($node);
}
}
================================================
FILE: src/Validation/Rule/NoFragmentCyclesRule.php
================================================
visitedFragments[$node->getNameValue()])) {
$this->detectFragmentCycle($node);
}
return new VisitorResult(null);
}
/**
* 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.
*
* @param FragmentDefinitionNode $fragment
*/
protected function detectFragmentCycle(FragmentDefinitionNode $fragment): void
{
$fragmentName = $fragment->getNameValue();
$this->visitedFragments[$fragmentName] = true;
$spreadNodes = $this->context->getFragmentSpreads($fragment->getSelectionSet());
if (empty($spreadNodes)) {
return;
}
$this->spreadPathIndexByName[$fragmentName] = \count($this->spreadPath);
foreach ($spreadNodes as $spreadNode) {
$spreadName = $spreadNode->getNameValue();
$cycleIndex = $this->spreadPathIndexByName[$spreadName] ?? null;
if (null === $cycleIndex) {
$this->spreadPath[] = $spreadNode;
if (!isset($this->visitedFragments[$spreadName])) {
$spreadFragment = $this->context->getFragment($spreadName);
if (null !== $spreadFragment) {
$this->detectFragmentCycle($spreadFragment);
}
}
\array_pop($this->spreadPath);
} else {
$cyclePath = \array_slice($this->spreadPath, $cycleIndex);
$this->context->reportError(
new ValidationException(
fragmentCycleMessage($spreadName, \array_map(function (FragmentSpreadNode $spread) {
return $spread->getNameValue();
}, $cyclePath)),
\array_merge($cyclePath, [$spreadNode])
)
);
}
}
$this->spreadPathIndexByName[$fragmentName] = null;
}
}
================================================
FILE: src/Validation/Rule/NoUndefinedVariablesRule.php
================================================
definedVariableNames = [];
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function enterVariableDefinition(VariableDefinitionNode $node): VisitorResult
{
$this->definedVariableNames[$node->getVariable()->getNameValue()] = true;
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function leaveOperationDefinition(OperationDefinitionNode $node): VisitorResult
{
$usages = $this->context->getRecursiveVariableUsages($node);
foreach ($usages as ['node' => $variableNode]) {
/** @var VariableNode $variableNode */
$variableName = $variableNode->getNameValue();
if (!isset($this->definedVariableNames[$variableName])) {
$operationName = $node->getName();
$this->context->reportError(
new ValidationException(
undefinedVariableMessage($variableName, $operationName ? $operationName->getValue() : null),
[$variableNode, $node]
)
);
}
}
return new VisitorResult($node);
}
}
================================================
FILE: src/Validation/Rule/NoUnusedFragmentsRule.php
================================================
operationDefinitions[] = $node;
return new VisitorResult(null);
}
/**
* @inheritdoc
*/
protected function enterFragmentDefinition(FragmentDefinitionNode $node): VisitorResult
{
$this->fragmentDefinitions[] = $node;
return new VisitorResult(null);
}
/**
* @inheritdoc
*/
protected function leaveDocument(DocumentNode $node): VisitorResult
{
$fragmentNamesUsed = [];
foreach ($this->operationDefinitions as $operationDefinition) {
foreach ($this->context->getRecursivelyReferencedFragments($operationDefinition) as $fragmentDefinition) {
$fragmentNamesUsed[$fragmentDefinition->getNameValue()] = true;
}
}
foreach ($this->fragmentDefinitions as $fragmentDefinition) {
$fragmentName = $fragmentDefinition->getNameValue();
if (!isset($fragmentNamesUsed[$fragmentName])) {
$this->context->reportError(
new ValidationException(unusedFragmentMessage($fragmentName), [$fragmentDefinition])
);
}
}
return new VisitorResult($node);
}
}
================================================
FILE: src/Validation/Rule/NoUnusedVariablesRule.php
================================================
variableDefinitions = [];
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function enterVariableDefinition(VariableDefinitionNode $node): VisitorResult
{
$this->variableDefinitions[] = $node;
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function leaveOperationDefinition(OperationDefinitionNode $node): VisitorResult
{
$variableNamesUsed = [];
$usages = $this->context->getRecursiveVariableUsages($node);
$operationNameNode = $node->getName();
$operationName = null !== $operationNameNode ? $operationNameNode->getValue() : null;
/** @var VariableNode $variableNode */
foreach ($usages as ['node' => $variableNode]) {
$variableNamesUsed[$variableNode->getNameValue()] = true;
}
foreach ($this->variableDefinitions as $variableDefinition) {
$variableName = $variableDefinition->getVariable()->getNameValue();
if (!isset($variableNamesUsed[$variableName])) {
$this->context->reportError(
new ValidationException(
unusedVariableMessage($variableName, $operationName),
[$variableDefinition]
)
);
}
}
return new VisitorResult($node);
}
}
================================================
FILE: src/Validation/Rule/OverlappingFieldsCanBeMergedRule.php
================================================
conflictFinder = new ConflictFinder();
}
/**
* @inheritdoc
*/
protected function enterSelectionSet(SelectionSetNode $node): VisitorResult
{
$this->conflictFinder->setContext($this->context);
$parentType = $this->context->getParentType();
$conflicts = $this->conflictFinder->findConflictsWithinSelectionSet($node, $parentType);
foreach ($conflicts as $conflict) {
$this->context->reportError(
new ValidationException(
fieldsConflictMessage($conflict->getResponseName(), $conflict->getReason()),
$conflict->getAllFields()
)
);
}
return new VisitorResult($node);
}
}
================================================
FILE: src/Validation/Rule/PossibleFragmentSpreadsRule.php
================================================
context->getType();
$parentType = $this->context->getParentType();
if ($fragmentType instanceof CompositeTypeInterface &&
$parentType instanceof CompositeTypeInterface &&
!TypeHelper::doTypesOverlap($this->context->getSchema(), $fragmentType, $parentType)
) {
$this->context->reportError(
new ValidationException(
typeIncompatibleAnonymousSpreadMessage((string)$parentType, (string)$fragmentType),
[$node]
)
);
}
return new VisitorResult($node);
}
/**
* @inheritdoc
*
* @throws InvariantException
* @throws ConversionException
*/
protected function enterFragmentSpread(FragmentSpreadNode $node): VisitorResult
{
$fragmentName = $node->getNameValue();
$fragmentType = $this->getFragmentType($fragmentName);
$parentType = $this->context->getParentType();
if (null !== $fragmentType &&
null !== $parentType &&
!TypeHelper::doTypesOverlap($this->context->getSchema(), $fragmentType, $parentType)
) {
$this->context->reportError(
new ValidationException(
typeIncompatibleSpreadMessage($fragmentName, (string)$parentType, (string)$fragmentType),
[$node]
)
);
}
return new VisitorResult($node);
}
/**
* @param string $name
* @return TypeInterface|null
* @throws InvariantException
* @throws ConversionException
*/
protected function getFragmentType(string $name): ?TypeInterface
{
$fragment = $this->context->getFragment($name);
if (null === $fragment) {
return null;
}
$type = TypeASTConverter::convert($this->context->getSchema(), $fragment->getTypeCondition());
return $type instanceof CompositeTypeInterface ? $type : null;
}
}
================================================
FILE: src/Validation/Rule/ProvidedRequiredArgumentsRule.php
================================================
context->getFieldDefinition();
if (null === $fieldDefinition) {
return new VisitorResult(null);
}
$argumentNodes = $node->getArguments();
$argumentNodeMap = keyMap($argumentNodes, function (ArgumentNode $argument) {
return $argument->getNameValue();
});
foreach ($fieldDefinition->getArguments() as $argumentDefinition) {
$argumentNode = $argumentNodeMap[$argumentDefinition->getName()] ?? null;
$argumentType = $argumentDefinition->getType();
$defaultValue = $argumentDefinition->getDefaultValue();
if (
null === $argumentNode
&& $argumentType instanceof NonNullType
&& null === $defaultValue
) {
$this->context->reportError(
new ValidationException(
missingFieldArgumentMessage(
(string)$node,
(string)$argumentDefinition,
(string)$argumentType
),
[$node]
)
);
}
}
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function leaveDirective(DirectiveNode $node): VisitorResult
{
$directiveDefinition = $this->context->getDirective();
if (null === $directiveDefinition) {
return new VisitorResult(null);
}
$argumentNodes = $node->getArguments();
$argumentNodeMap = keyMap($argumentNodes, function (ArgumentNode $argument) {
return $argument->getNameValue();
});
foreach ($directiveDefinition->getArguments() as $argumentDefinition) {
$argumentNode = $argumentNodeMap[$argumentDefinition->getName()] ?? null;
$argumentType = $argumentDefinition->getType();
if (null === $argumentNode && $argumentType instanceof NonNullType) {
$this->context->reportError(
new ValidationException(
missingDirectiveArgumentMessage(
(string)$node,
(string)$argumentDefinition,
(string)$argumentType
),
[$node]
)
);
}
}
return new VisitorResult($node);
}
}
================================================
FILE: src/Validation/Rule/RuleInterface.php
================================================
context->getType();
$selectionSet = $node->getSelectionSet();
if (null !== $type) {
if (getNamedType($type) instanceof LeafTypeInterface) {
if (null !== $selectionSet) {
$this->context->reportError(
new ValidationException(
noSubselectionAllowedMessage((string)$node, (string)$type),
[$selectionSet]
)
);
}
} elseif (null === $selectionSet) {
$this->context->reportError(
new ValidationException(
requiresSubselectionMessage((string)$node, (string)$type),
[$node]
)
);
}
}
return new VisitorResult($node);
}
}
================================================
FILE: src/Validation/Rule/SingleFieldSubscriptionsRule.php
================================================
getOperation() !== 'subscription') {
return new VisitorResult($node);
}
$selectionSet = $node->getSelectionSet();
if (null !== $selectionSet) {
$selections = $selectionSet->getSelections();
if (\count($selections) !== 1) {
$this->context->reportError(
new ValidationException(singleFieldOnlyMessage((string)$node), \array_slice($selections, 1))
);
}
}
return new VisitorResult($node);
}
}
================================================
FILE: src/Validation/Rule/SupportedRules.php
================================================
knownArgumentNames = [];
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function enterDirective(DirectiveNode $node): VisitorResult
{
$this->knownArgumentNames = [];
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function enterArgument(ArgumentNode $node): VisitorResult
{
$argumentName = $node->getNameValue();
if (isset($this->knownArgumentNames[$argumentName])) {
$this->context->reportError(
new ValidationException(
duplicateArgumentMessage($argumentName),
[$this->knownArgumentNames[$argumentName], $node->getName()]
)
);
} else {
$this->knownArgumentNames[$argumentName] = $node->getName();
}
return new VisitorResult(null);
}
}
================================================
FILE: src/Validation/Rule/UniqueDirectivesPerLocationRule.php
================================================
getDirectives();
$knownDirectives = [];
foreach ($directives as $directive) {
$directiveName = $directive->getNameValue();
if (isset($knownDirectives[$directiveName])) {
$this->context->reportError(
new ValidationException(
duplicateDirectiveMessage($directiveName),
[$knownDirectives[$directiveName], $directive]
)
);
} else {
$knownDirectives[$directiveName] = $directive;
}
}
}
return new VisitorResult($node);
}
}
================================================
FILE: src/Validation/Rule/UniqueFragmentNamesRule.php
================================================
getNameValue();
if (isset($this->knownFragmentNames[$fragmentName])) {
$this->context->reportError(
new ValidationException(
duplicateFragmentMessage($fragmentName),
[$this->knownFragmentNames[$fragmentName], $node->getName()]
)
);
} else {
$this->knownFragmentNames[$fragmentName] = $node->getName();
}
return new VisitorResult(null);
}
}
================================================
FILE: src/Validation/Rule/UniqueInputFieldNamesRule.php
================================================
knownInputNamesStack[] = $this->knownInputNames;
$this->knownInputNames = [];
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function enterObjectField(ObjectFieldNode $node): VisitorResult
{
$fieldName = $node->getNameValue();
if (isset($this->knownInputNames[$fieldName])) {
$this->context->reportError(
new ValidationException(
duplicateInputFieldMessage($fieldName),
[$this->knownInputNames[$fieldName], $node->getName()]
)
);
} else {
$this->knownInputNames[$fieldName] = $node->getName();
}
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function leaveObjectValue(ObjectValueNode $node): VisitorResult
{
$this->knownInputNames = \array_pop($this->knownInputNamesStack);
return new VisitorResult($node);
}
}
================================================
FILE: src/Validation/Rule/UniqueOperationNamesRule.php
================================================
getNameValue();
if (null !== $operationName) {
if (isset($this->knownOperationNames[$operationName])) {
$this->context->reportError(
new ValidationException(
duplicateOperationMessage($operationName),
[$this->knownOperationNames[$operationName], $node->getName()]
)
);
} else {
$this->knownOperationNames[$operationName] = $node->getName();
}
}
return new VisitorResult($node);
}
}
================================================
FILE: src/Validation/Rule/UniqueVariableNamesRule.php
================================================
knownVariableNames = [];
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function enterVariableDefinition(VariableDefinitionNode $node): VisitorResult
{
$variable = $node->getVariable();
$variableName = $variable->getNameValue();
if (isset($this->knownVariableNames[$variableName])) {
$this->context->reportError(
new ValidationException(
duplicateVariableMessage($variableName),
[$this->knownVariableNames[$variableName], $variable->getName()]
)
);
} else {
$this->knownVariableNames[$variableName] = $variable->getName();
}
return new VisitorResult($node);
}
}
================================================
FILE: src/Validation/Rule/ValuesOfCorrectTypeRule.php
================================================
context->getInputType();
if ($type instanceof NonNullType) {
$this->context->reportError(
new ValidationException(
badValueMessage((string)$type, printNode($node)),
[$node]
)
);
}
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function enterListValue(ListValueNode $node): VisitorResult
{
// 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 = getNullableType($this->context->getParentInputType());
if (!($type instanceof ListType)) {
$this->isValidScalar($node);
return new VisitorResult(null);
}
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function enterObjectField(ObjectFieldNode $node): VisitorResult
{
$parentType = getNamedType($this->context->getParentInputType());
$fieldType = $this->context->getInputType();
if (null === $fieldType && $parentType instanceof InputObjectType) {
$suggestions = suggestionList($node->getNameValue(), \array_keys($parentType->getFields()));
$didYouMean = !empty($suggestions) ? \sprintf('Did you mean %s?', orList($suggestions)) : null;
$this->context->reportError(
new ValidationException(
unknownFieldMessage($parentType->getName(), $node->getNameValue(), $didYouMean),
[$node]
)
);
}
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function enterObjectValue(ObjectValueNode $node): VisitorResult
{
$type = getNamedType($this->context->getInputType());
if (!($type instanceof InputObjectType)) {
$this->isValidScalar($node);
return new VisitorResult(null);
}
// Ensure every required field exists.
$inputFields = $type->getFields();
$fieldNodeMap = keyMap($node->getFields(), function (ObjectFieldNode $field) {
return $field->getNameValue();
});
foreach ($inputFields as $fieldName => $field) {
$fieldType = $field->getType();
$fieldNode = $fieldNodeMap[$fieldName] ?? null;
$fieldDefaultValue = $field->getDefaultValue();
if (null === $fieldNode
&& $fieldType instanceof NonNullType
&& null === $fieldDefaultValue
) {
$this->context->reportError(
new ValidationException(
requiredFieldMessage($type->getName(), $fieldName, (string)$fieldType),
[$node]
)
);
}
}
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function enterEnumValue(EnumValueNode $node): VisitorResult
{
$type = getNamedType($this->context->getInputType());
if (!($type instanceof EnumType)) {
$this->isValidScalar($node);
} elseif (!$type->getValue($node->getValue())) {
$didYouMean = $this->getEnumTypeSuggestion($type, $node);
$this->context->reportError(
new ValidationException(
badValueMessage($type->getName(), printNode($node), $didYouMean),
[$node]
)
);
}
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function enterIntValue(IntValueNode $node): VisitorResult
{
$this->isValidScalar($node);
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function enterFloatValue(FloatValueNode $node): VisitorResult
{
$this->isValidScalar($node);
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function enterStringValue(StringValueNode $node): VisitorResult
{
$this->isValidScalar($node);
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function enterBooleanValue(BooleanValueNode $node): VisitorResult
{
$this->isValidScalar($node);
return new VisitorResult($node);
}
/**
* Any value literal may be a valid representation of a Scalar, depending on
* that scalar type.
*
* @param ValueNodeInterface $node
* @throws InvariantException
*/
protected function isValidScalar(ValueNodeInterface $node): void
{
$locationType = $this->context->getInputType();
if (null === $locationType) {
return;
}
$type = getNamedType($locationType);
if (!($type instanceof ScalarType)) {
$didYouMean = $this->getEnumTypeSuggestion($type, $node) ?? null;
$this->context->reportError(
new ValidationException(
badValueMessage((string)$locationType, printNode($node), $didYouMean),
[$node]
)
);
return;
}
// Scalars determine if a literal value is valid via parseLiteral() which
// may throw or return an invalid value to indicate failure.
try {
$result = $type->parseLiteral($node, null/* $variables */);
if (null === $result) {
$this->context->reportError(
new ValidationException(
badValueMessage((string)$locationType, printNode($node)),
[$node]
)
);
}
} catch (\Exception $ex) {
// Ensure a reference to the original error is maintained.
$this->context->reportError(
new ValidationException(
badValueMessage((string)$locationType, printNode($node), $ex->getMessage()),
[$node],
null,
null,
null,
null,
$ex
)
);
}
}
/**
* @param NamedTypeInterface $type
* @param ValueNodeInterface $node
* @return null|string
* @throws InvariantException
*/
protected function getEnumTypeSuggestion(NamedTypeInterface $type, ValueNodeInterface $node): ?string
{
if ($type instanceof EnumType) {
$suggestions = suggestionList(printNode($node), \array_map(function (EnumValue $value) {
return $value->getName();
}, $type->getValues()));
return !empty($suggestions)
? \sprintf('Did you mean the enum value %s?', orList($suggestions))
: null;
}
return null;
}
}
================================================
FILE: src/Validation/Rule/VariablesAreInputTypesRule.php
================================================
context->getSchema(), $node->getType());
if (!isInputType($type)) {
$variableName = $node->getVariable()->getNameValue();
$this->context->reportError(
new ValidationException(
nonInputTypeOnVariableMessage($variableName, printNode($node->getType())),
[$node->getType()]
)
);
}
return new VisitorResult($node);
}
}
================================================
FILE: src/Validation/Rule/VariablesDefaultValueAllowedRule.php
================================================
getVariable();
$variableName = $variable->getNameValue();
$defaultValue = $node->getDefaultValue();
$type = $this->context->getInputType();
if (null !== $defaultValue && $type instanceof NonNullType) {
$this->context->reportError(
new ValidationException(
variableDefaultValueNotAllowedMessage($variableName, $type, (string)$type->getOfType()),
[$defaultValue]
)
);
}
return new VisitorResult(null); // do not traverse further.
}
}
================================================
FILE: src/Validation/Rule/VariablesInAllowedPositionRule.php
================================================
variableDefinitionMap = [];
return new VisitorResult($node);
}
/**
* @inheritdoc
*
* @throws InvariantException
* @throws \Digia\GraphQL\Util\ConversionException
*/
protected function leaveOperationDefinition(OperationDefinitionNode $node): VisitorResult
{
$usages = $this->context->getRecursiveVariableUsages($node);
/**
* @var VariableNode $variableNode
* @var TypeInterface $type
*/
foreach ($usages as ['node' => $variableNode, 'type' => $type]) {
$variableName = $variableNode->getNameValue();
$variableDefinition = $this->variableDefinitionMap[$variableName];
if (null !== $variableDefinition && null !== $type) {
// A var type is allowed if it is the same or more strict (e.g. is
// a subtype of) than the expected type. It can be more strict if
// the variable type is non-null when the expected type is nullable.
// If both are list types, the variable item type can be more strict
// than the expected item type (contravariant).
$schema = $this->context->getSchema();
$variableType = TypeASTConverter::convert($schema, $variableDefinition->getType());
if (null !== $variableType &&
!TypeHelper::isTypeSubtypeOf(
$schema,
$this->getEffectiveType($variableType, $variableDefinition),
$type
)
) {
$this->context->reportError(
new ValidationException(
badVariablePositionMessage($variableName, (string)$variableType, (string)$type),
[$variableDefinition, $variableNode]
)
);
}
}
}
return new VisitorResult($node);
}
/**
* @inheritdoc
*/
protected function enterVariableDefinition(VariableDefinitionNode $node): VisitorResult
{
$this->variableDefinitionMap[$node->getVariable()->getNameValue()] = $node;
return new VisitorResult($node);
}
/**
* If a variable definition has a default value, it's effectively non-null.
*
* @param TypeInterface $variableType
* @param VariableDefinitionNode $variableDefinition
* @return TypeInterface
*
* @throws InvariantException
*/
protected function getEffectiveType(
TypeInterface $variableType,
VariableDefinitionNode $variableDefinition
): TypeInterface {
return (!$variableDefinition->hasDefaultValue() || $variableType instanceof NonNullType)
? $variableType
: newNonNull($variableType);
}
}
================================================
FILE: src/Validation/RulesProvider.php
================================================
container->add(ExecutableDefinitionsRule::class, ExecutableDefinitionsRule::class);
$this->container->add(FieldOnCorrectTypeRule::class, FieldOnCorrectTypeRule::class);
$this->container->add(FragmentsOnCompositeTypesRule::class, FragmentsOnCompositeTypesRule::class);
$this->container->add(KnownArgumentNamesRule::class, KnownArgumentNamesRule::class);
$this->container->add(KnownDirectivesRule::class, KnownDirectivesRule::class);
$this->container->add(KnownFragmentNamesRule::class, KnownFragmentNamesRule::class);
$this->container->add(KnownTypeNamesRule::class, KnownTypeNamesRule::class);
$this->container->add(LoneAnonymousOperationRule::class, LoneAnonymousOperationRule::class);
$this->container->add(NoFragmentCyclesRule::class, NoFragmentCyclesRule::class);
$this->container->add(NoUndefinedVariablesRule::class, NoUndefinedVariablesRule::class);
$this->container->add(NoUnusedFragmentsRule::class, NoUnusedFragmentsRule::class);
$this->container->add(NoUnusedVariablesRule::class, NoUnusedVariablesRule::class);
$this->container->add(OverlappingFieldsCanBeMergedRule::class, OverlappingFieldsCanBeMergedRule::class);
$this->container->add(PossibleFragmentSpreadsRule::class, PossibleFragmentSpreadsRule::class);
$this->container->add(ProvidedRequiredArgumentsRule::class, ProvidedRequiredArgumentsRule::class);
$this->container->add(ScalarLeafsRule::class, ScalarLeafsRule::class);
$this->container->add(SingleFieldSubscriptionsRule::class, SingleFieldSubscriptionsRule::class);
$this->container->add(UniqueArgumentNamesRule::class, UniqueArgumentNamesRule::class);
$this->container->add(UniqueDirectivesPerLocationRule::class, UniqueDirectivesPerLocationRule::class);
$this->container->add(UniqueFragmentNamesRule::class, UniqueFragmentNamesRule::class);
$this->container->add(UniqueInputFieldNamesRule::class, UniqueInputFieldNamesRule::class);
$this->container->add(UniqueOperationNamesRule::class, UniqueOperationNamesRule::class);
$this->container->add(UniqueVariableNamesRule::class, UniqueVariableNamesRule::class);
$this->container->add(ValuesOfCorrectTypeRule::class, ValuesOfCorrectTypeRule::class);
$this->container->add(VariablesAreInputTypesRule::class, VariablesAreInputTypesRule::class);
$this->container->add(VariablesDefaultValueAllowedRule::class, VariablesDefaultValueAllowedRule::class);
$this->container->add(VariablesInAllowedPositionRule::class, VariablesInAllowedPositionRule::class);
}
}
================================================
FILE: src/Validation/ValidationContext.php
================================================
schema = $schema;
$this->document = $document;
$this->typeInfo = $typeInfo;
}
/**
* @inheritdoc
*/
public function reportError(ValidationException $error): void
{
$this->errors[] = $error;
}
/**
* @inheritdoc
*/
public function getErrors(): array
{
return $this->errors;
}
/**
* @return TypeInterface|null
*/
public function getType(): ?TypeInterface
{
return $this->typeInfo->getType();
}
/**
* @inheritdoc
*/
public function getParentType(): ?TypeInterface
{
return $this->typeInfo->getParentType();
}
/**
* @inheritdoc
*/
public function getInputType(): ?TypeInterface
{
return $this->typeInfo->getInputType();
}
/**
* @inheritdoc
*/
public function getParentInputType(): ?TypeInterface
{
return $this->typeInfo->getParentInputType();
}
/**
* @inheritdoc
*/
public function getFieldDefinition(): ?Field
{
return $this->typeInfo->getFieldDefinition();
}
/**
* @inheritdoc
*/
public function getSchema(): Schema
{
return $this->schema;
}
/**
* @inheritdoc
*/
public function getArgument(): ?Argument
{
return $this->typeInfo->getArgument();
}
/**
* @inheritdoc
*/
public function getDirective(): ?Directive
{
return $this->typeInfo->getDirective();
}
/**
* @inheritdoc
*/
public function getFragment(string $name): ?FragmentDefinitionNode
{
if (empty($this->fragments)) {
$this->fragments = array_reduce($this->document->getDefinitions(), function ($fragments, $definition) {
if ($definition instanceof FragmentDefinitionNode) {
$fragments[$definition->getNameValue()] = $definition;
}
return $fragments;
}, []);
}
return $this->fragments[$name] ?? null;
}
/**
* @inheritdoc
*/
public function getFragmentSpreads(SelectionSetNode $selectionSet): array
{
$spreads = $this->fragmentSpreads[(string)$selectionSet] ?? null;
if (null === $spreads) {
$spreads = [];
$setsToVisit = [$selectionSet];
while (!empty($setsToVisit)) {
/** @var SelectionSetNode $set */
$set = array_pop($setsToVisit);
foreach ($set->getSelections() as $selection) {
if ($selection instanceof FragmentSpreadNode) {
$spreads[] = $selection;
} elseif ($selection instanceof SelectionSetAwareInterface && $selection->hasSelectionSet()) {
$setsToVisit[] = $selection->getSelectionSet();
}
}
}
$this->fragmentSpreads[(string)$selectionSet] = $spreads;
}
return $spreads;
}
/**
* @inheritdoc
*/
public function getRecursiveVariableUsages(OperationDefinitionNode $operation): array
{
$usages = $this->recursiveVariableUsages[(string)$operation] ?? null;
if (null === $usages) {
$usages = $this->getVariableUsages($operation);
$fragments = $this->getRecursivelyReferencedFragments($operation);
foreach ($fragments as $fragment) {
// TODO: Figure out a more performance way to do this.
/** @noinspection SlowArrayOperationsInLoopInspection */
$usages = array_merge($usages, $this->getVariableUsages($fragment));
}
$this->recursiveVariableUsages[(string)$operation] = $usages;
}
return $usages;
}
/**
* @inheritdoc
*/
public function getVariableUsages(NodeInterface $node): array
{
$usages = $this->variableUsages[(string)$node] ?? null;
if (null === $usages) {
$usages = [];
$typeInfo = new TypeInfo($this->schema);
$enterCallback = function (NodeInterface $node) use (&$usages, $typeInfo): VisitorResult {
if ($node instanceof VariableDefinitionNode) {
return new VisitorResult(null);
}
if ($node instanceof VariableNode) {
$usages[] = [
'node' => $node,
'type' => $typeInfo->getInputType(),
'defaultValue' => $typeInfo->getDefaultValue(),
];
}
return new VisitorResult($node);
};
$visitor = new TypeInfoVisitor($typeInfo, new Visitor($enterCallback));
$node->acceptVisitor(new VisitorInfo($visitor));
$this->variableUsages[(string)$node] = $usages;
}
return $usages;
}
/**
* @inheritdoc
*/
public function getRecursivelyReferencedFragments(OperationDefinitionNode $operation): array
{
$fragments = $this->recursivelyReferencedFragment[(string)$operation] ?? null;
if (null === $fragments) {
$fragments = [];
$collectedNames = [];
$nodesToVisit = [$operation->getSelectionSet()];
while (!empty($nodesToVisit)) {
$node = array_pop($nodesToVisit);
$spreads = $this->getFragmentSpreads($node);
foreach ($spreads as $spread) {
$fragmentName = $spread->getNameValue();
if (!isset($collectedNames[$fragmentName])) {
$collectedNames[$fragmentName] = true;
$fragment = $this->getFragment($fragmentName);
if (null !== $fragment) {
$fragments[] = $fragment;
$nodesToVisit[] = $fragment->getSelectionSet();
}
}
}
}
$this->recursivelyReferencedFragment[(string)$operation] = $fragments;
}
return $fragments;
}
}
================================================
FILE: src/Validation/ValidationContextAwareTrait.php
================================================
context;
}
/**
* @param ValidationContextInterface $context
* @return $this
*/
public function setContext(ValidationContextInterface $context)
{
$this->context = $context;
return $this;
}
}
================================================
FILE: src/Validation/ValidationContextInterface.php
================================================
container->share(ValidatorInterface::class, Validator::class);
}
}
================================================
FILE: src/Validation/Validator.php
================================================
createContext($schema, $document, $typeInfo);
$visitors = \array_map(function (RuleInterface $rule) use ($context) {
return $rule->setContext($context);
}, $rules);
$visitor = new TypeInfoVisitor($typeInfo, new ParallelVisitor($visitors));
// Visit the whole document with each instance of all provided rules.
/** @noinspection PhpUnhandledExceptionInspection */
$document->acceptVisitor(new VisitorInfo($visitor));
return $context->getErrors();
}
/**
* @param Schema $schema
* @param DocumentNode $document
* @param TypeInfo $typeInfo
* @return ValidationContextInterface
*/
public function createContext(
Schema $schema,
DocumentNode $document,
TypeInfo $typeInfo
): ValidationContextInterface {
return new ValidationContext($schema, $document, $typeInfo);
}
}
================================================
FILE: src/Validation/ValidatorInterface.php
================================================
build();
}
return GraphQL::buildSchema($source, $resolverRegistry, $options);
}
/**
* @param Schema $schema
* @param string|Source $source
* @param array|ResolverRegistryInterface $resolverRegistry
* @param array $options
* @return Schema
* @throws InvariantException
*/
function extendSchema(Schema $schema, $source, $resolverRegistry = [], array $options = []): Schema
{
if (\is_string($source)) {
$source = (new StringSourceBuilder($source))->build();
}
return GraphQL::extendSchema($schema, $source, $resolverRegistry, $options);
}
/**
* @param Schema $schema
* @return SchemaValidationException[]
*/
function validateSchema(Schema $schema): array
{
return GraphQL::validateSchema($schema);
}
/**
* @param string|Source $source
* @param array $options
* @return DocumentNode
* @throws InvariantException
* @throws SyntaxErrorException
*/
function parse($source, array $options = []): DocumentNode
{
if (\is_string($source)) {
$source = (new StringSourceBuilder($source))->build();
}
return GraphQL::parse($source, $options);
}
/**
* @param string|Source $source
* @param array $options
* @return ValueNodeInterface
* @throws InvariantException
* @throws SyntaxErrorException
*/
function parseValue($source, array $options = []): ValueNodeInterface
{
if (\is_string($source)) {
$source = (new StringSourceBuilder($source))->build();
}
return GraphQL::parseValue($source, $options);
}
/**
* @param string|Source $source
* @param array $options
* @return TypeNodeInterface
* @throws InvariantException
* @throws SyntaxErrorException
*/
function parseType($source, array $options = []): TypeNodeInterface
{
if (\is_string($source)) {
$source = (new StringSourceBuilder($source))->build();
}
return GraphQL::parseType($source, $options);
}
/**
* @param Schema $schema
* @param DocumentNode $document
* @return array|ValidationException[]
*/
function validate(Schema $schema, DocumentNode $document): array
{
return GraphQL::validate($schema, $document);
}
/**
* @param Schema $schema
* @param DocumentNode $document
* @param mixed|null $rootValue
* @param mixed|null $contextValue
* @param array $variableValues
* @param mixed|null $operationName
* @param callable|null $fieldResolver
* @param ErrorHandlerInterface|null $errorHandler
* @return ExecutionResult
*/
function execute(
Schema $schema,
DocumentNode $document,
$rootValue = null,
$contextValue = null,
array $variableValues = [],
$operationName = null,
callable $fieldResolver = null,
$errorHandler = null
): ExecutionResult {
$resultPromise = GraphQL::execute(
$schema,
$document,
$rootValue,
$contextValue,
$variableValues,
$operationName,
$fieldResolver,
$errorHandler
);
$data = null;
$resultPromise->then(function (ExecutionResult $result) use (&$data) {
$data = $result;
});
if (null === $data) {
$data = new ExecutionResult(null, [
new GraphQLException('Looks like you are using Event Loop. Please use `executeAsync` method instead.')
]);
}
return $data;
}
/**
* @param Schema $schema
* @param DocumentNode $document
* @param mixed|null $rootValue
* @param mixed|null $contextValue
* @param array $variableValues
* @param mixed|null $operationName
* @param callable|null $fieldResolver
* @param ErrorHandlerInterface|null $errorHandler
* @return PromiseInterface
*/
function executeAsync(
Schema $schema,
DocumentNode $document,
$rootValue = null,
$contextValue = null,
array $variableValues = [],
$operationName = null,
callable $fieldResolver = null,
$errorHandler = null
): PromiseInterface {
return GraphQL::execute(
$schema,
$document,
$rootValue,
$contextValue,
$variableValues,
$operationName,
$fieldResolver,
$errorHandler
);
}
/**
* @param NodeInterface $node
* @return string
*/
function printNode(NodeInterface $node): string
{
return GraphQL::print($node);
}
/**
* @param Schema $schema
* @param string $source
* @param mixed $rootValue
* @param mixed $contextValue
* @param array $variableValues
* @param string|null $operationName
* @param callable|null $fieldResolver
* @param ErrorHandlerInterface|callable|null $errorHandler
* @return array
* @throws InvariantException
* @throws SyntaxErrorException
*/
function graphql(
Schema $schema,
string $source,
$rootValue = null,
$contextValue = null,
array $variableValues = [],
?string $operationName = null,
?callable $fieldResolver = null,
$errorHandler = null
): array {
if (\is_callable($errorHandler)) {
$errorHandler = new ErrorHandler([new CallableMiddleware($errorHandler)]);
}
$resultPromise = GraphQL::process(
$schema,
$source,
$rootValue,
$contextValue,
$variableValues,
$operationName,
$fieldResolver,
$errorHandler
);
$data = null;
$resultPromise->then(function (ExecutionResult $result) use (&$data) {
$data = $result;
});
if (null === $data) {
$data = new ExecutionResult(null, [
new GraphQLException('Looks like you are using Event Loop. Please use `graphqlAsync` method instead.')
]);
}
return $data->toArray();
}
/**
* @param Schema $schema
* @param string $source
* @param mixed $rootValue
* @param mixed $contextValue
* @param array $variableValues
* @param string|null $operationName
* @param callable|null $fieldResolver
* @param ErrorHandlerInterface|callable|null $errorHandler
* @return PromiseInterface
* @throws InvariantException
* @throws SyntaxErrorException
*/
function graphqlAsync(
Schema $schema,
string $source,
$rootValue = null,
$contextValue = null,
array $variableValues = [],
?string $operationName = null,
?callable $fieldResolver = null,
$errorHandler = null
): PromiseInterface {
if (\is_callable($errorHandler)) {
$errorHandler = new ErrorHandler([new CallableMiddleware($errorHandler)]);
}
$resultPromise = GraphQL::process(
$schema,
$source,
$rootValue,
$contextValue,
$variableValues,
$operationName,
$fieldResolver,
$errorHandler
);
return $resultPromise->then(function (ExecutionResult $result) {
return $result->toArray();
});
}
================================================
FILE: tests/Functional/Execution/AbstractPromiseTest.php
================================================
'Pet',
'fields' => [
'name' => ['type' => stringType()]
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$DogType = newObjectType([
'name' => 'Dog',
'interfaces' => [$PetInterfaceType],
'fields' => [
'name' => ['type' => stringType()],
'woofs' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return \React\Promise\resolve($obj instanceof Dog);
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$CatType = newObjectType([
'name' => 'Cat',
'interfaces' => [$PetInterfaceType],
'fields' => [
'name' => ['type' => stringType()],
'meows' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return \React\Promise\resolve($obj instanceof Cat);
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => newList($PetInterfaceType),
'resolve' => function ($source, $args, $context, $info) {
return [
new Dog('Odie', true),
new Cat('Garfield', false)
];
}
]
],
]),
'types' => [$DogType, $CatType],
]);
$source = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($source));
$expected = new ExecutionResult([
'pets' => [
[
'name' => 'Odie',
'woofs' => true,
],
[
'name' => 'Garfield',
'meows' => false,
]
]
], []);
$this->assertEquals($expected, $result);
}
/**
* isTypeOf can be rejected
*/
public function testIsTypeOfCanBeRejected()
{
/** @noinspection PhpUnhandledExceptionInspection */
$PetInterfaceType = newInterfaceType([
'name' => 'Pet',
'fields' => [
'name' => ['type' => stringType()]
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$DogType = newObjectType([
'name' => 'Dog',
'interfaces' => [$PetInterfaceType],
'fields' => [
'name' => ['type' => stringType()],
'woofs' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return \React\Promise\reject(new ExecutionException('We are testing this error'));
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$CatType = newObjectType([
'name' => 'Cat',
'interfaces' => [$PetInterfaceType],
'fields' => [
'name' => ['type' => stringType()],
'meows' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return \React\Promise\resolve($obj instanceof Cat);
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => newList($PetInterfaceType),
'resolve' => function ($source, $args, $context, $info) {
return [
new Dog('Odie', true),
new Cat('Garfield', false)
];
}
]
],
]),
'types' => [$DogType, $CatType],
]);
$source = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($source));
$expected = [
'data' => [
'pets' => [
null,
null
]
],
'errors' => [
[
'message' => 'We are testing this error',
'locations' => null,
'path' => ['pets', 0]
],
[
'message' => 'We are testing this error',
'locations' => null,
'path' => ['pets', 1]
]
]
];
$this->assertEquals($expected, $result->toArray());
}
/**
* isTypeOf used to resolve runtime type for Union
*/
public function testIsTypeOfUsedToResolveRuntimeTypeForUnion()
{
/** @noinspection PhpUnhandledExceptionInspection */
$DogType = newObjectType([
'name' => 'Dog',
'fields' => [
'name' => ['type' => stringType()],
'woofs' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return \React\Promise\resolve($obj instanceof Dog);
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$CatType = newObjectType([
'name' => 'Cat',
'fields' => [
'name' => ['type' => stringType()],
'meows' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return \React\Promise\resolve($obj instanceof Cat);
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$PetUnionType = newUnionType([
'name' => 'Pet',
'types' => [$DogType, $CatType],
'resolveType' => function ($result, $context, $info) use ($DogType, $CatType) {
if ($result instanceof Dog) {
return $DogType;
}
if ($result instanceof Cat) {
return $CatType;
}
return null;
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => newList($PetUnionType),
'resolve' => function ($source, $args, $context, $info) {
return [
new Dog('Odie', true),
new Cat('Garfield', false)
];
}
]
]
]),
]);
$source = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($source));
$expected = new ExecutionResult([
'pets' => [
[
'name' => 'Odie',
'woofs' => true,
],
[
'name' => 'Garfield',
'meows' => false,
]
]
], []);
$this->assertEquals($expected, $result);
}
/**
* resolveType on Interface yields useful error
*/
public function testResolveTypeOnInterfaceYieldsUsefulError()
{
/** @noinspection PhpUnhandledExceptionInspection */
$PetInterfaceType = newInterfaceType([
'name' => 'Pet',
'resolveType' => function ($obj) use (&$DogType, &$CatType, &$HumanType) {
return \React\Promise\resolve(
$obj instanceof Dog
? $DogType
: ($obj instanceof Cat
? $CatType
: ($obj instanceof Human ? $HumanType : null)));
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$DogType = newObjectType([
'name' => 'Dog',
'interfaces' => [$PetInterfaceType],
'fields' => [
'name' => ['type' => stringType()],
'woofs' => ['type' => booleanType()],
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$CatType = newObjectType([
'name' => 'Cat',
'interfaces' => [$PetInterfaceType],
'fields' => [
'name' => ['type' => stringType()],
'meows' => ['type' => booleanType()],
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$HumanType = newObjectType([
'name' => 'Human',
'fields' => [
'name' => ['type' => stringType()]
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => newList($PetInterfaceType),
'resolve' => function ($source, $args, $context, $info) {
return \React\Promise\resolve([
new Dog('Odie', true),
new Cat('Garfield', false),
new Human('John')
]);
}
]
]
]),
'types' => [$DogType, $CatType]
]);
$source = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($source));
$expected = [
'data' => [
'pets' => [
[
'name' => 'Odie',
'woofs' => true,
],
[
'name' => 'Garfield',
'meows' => false,
],
null
]
],
'errors' => [
[
'message' => 'Runtime Object type "Human" is not a possible type for "Pet".',
'locations' => null,
'path' => ['pets', 2]
],
]
];
$this->assertEquals($expected, $result->toArray());
}
/**
* resolveType on Union yields useful error
*/
public function testResolveTypeOnUnionYieldsUsefulError()
{
/** @noinspection PhpUnhandledExceptionInspection */
$DogType = newObjectType([
'name' => 'Dog',
'fields' => [
'name' => ['type' => stringType()],
'woofs' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return $obj instanceof Dog;
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$CatType = newObjectType([
'name' => 'Cat',
'fields' => [
'name' => ['type' => stringType()],
'meows' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return $obj instanceof Cat;
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$HumanType = newObjectType([
'name' => 'Human',
'fields' => [
'name' => ['type' => stringType()]
],
'isTypeOf' => function ($obj) {
return $obj instanceof Human;
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$PetUnionType = newUnionType([
'name' => 'Pet',
'types' => [$DogType, $CatType],
'resolveType' => function ($result, $context, $info) use ($DogType, $CatType, $HumanType) {
if ($result instanceof Dog) {
return $DogType;
}
if ($result instanceof Cat) {
return $CatType;
}
if ($result instanceof Human) {
return $HumanType;
}
return null;
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => newList($PetUnionType),
'resolve' => function ($source, $args, $context, $info) {
return \React\Promise\resolve([
new Dog('Odie', true),
new Cat('Garfield', false),
new Human('John')
]);
}
]
]
]),
]);
$source = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($source));
$expected = [
'data' => [
'pets' => [
[
'name' => 'Odie',
'woofs' => true,
],
[
'name' => 'Garfield',
'meows' => false,
],
null
]
],
'errors' => [
[
'message' => 'Runtime Object type "Human" is not a possible type for "Pet".',
'locations' => null,
'path' => ['pets', 2]
],
]
];
$this->assertArraySubset($expected, $result->toArray());
}
/**
* resolveType allows resolving with type name
*/
public function testResolveTypeAllowsResolvingWithTypeName()
{
/** @noinspection PhpUnhandledExceptionInspection */
$PetInterfaceType = newInterfaceType([
'name' => 'Pet',
'resolveType' => function ($obj) {
return \React\Promise\resolve(
$obj instanceof Dog
? 'Dog'
: ($obj instanceof Cat ? 'Cat' : null)
);
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$DogType = newObjectType([
'name' => 'Dog',
'interfaces' => [$PetInterfaceType],
'fields' => [
'name' => ['type' => stringType()],
'woofs' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return $obj instanceof Dog;
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$CatType = newObjectType([
'name' => 'Cat',
'interfaces' => [$PetInterfaceType],
'fields' => [
'name' => ['type' => stringType()],
'meows' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return $obj instanceof Cat;
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => newList($PetInterfaceType),
'resolve' => function ($source, $args, $context, $info) {
return [
new Dog('Odie', true),
new Cat('Garfield', false)
];
}
]
]
]),
'types' => [$DogType, $CatType]
]);
$source = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($source));
$expected = new ExecutionResult([
'pets' => [
[
'name' => 'Odie',
'woofs' => true,
],
[
'name' => 'Garfield',
'meows' => false,
]
]
], []);
$this->assertEquals($expected, $result);
}
/**
* resolveType can be caught
*/
public function testResolveTypeCanBeCaught()
{
/** @noinspection PhpUnhandledExceptionInspection */
$PetInterfaceType = newInterfaceType([
'name' => 'Pet',
'resolveType' => function ($obj) {
return \React\Promise\reject(new ExecutionException('We are testing this error'));
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$DogType = newObjectType([
'name' => 'Dog',
'interfaces' => [$PetInterfaceType],
'fields' => [
'name' => ['type' => stringType()],
'woofs' => ['type' => booleanType()],
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$CatType = newObjectType([
'name' => 'Cat',
'interfaces' => [$PetInterfaceType],
'fields' => [
'name' => ['type' => stringType()],
'meows' => ['type' => booleanType()],
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => newList($PetInterfaceType),
'resolve' => function ($source, $args, $context, $info) {
return [
new Dog('Odie', true),
new Cat('Garfield', false)
];
}
]
]
]),
'types' => [$DogType, $CatType]
]);
$source = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($source));
$expected = [
'data' => [
'pets' => [
null,
null
]
],
'errors' => [
[
'message' => 'We are testing this error',
'locations' => null,
'path' => ['pets', 0]
],
[
'message' => 'We are testing this error',
'locations' => null,
'path' => ['pets', 1]
]
]
];
$this->assertEquals($expected, $result->toArray());
}
}
================================================
FILE: tests/Functional/Execution/AbstractTest.php
================================================
'Pet',
'fields' => [
'name' => ['type' => stringType()]
]
]);
$DogType = newObjectType([
'name' => 'Dog',
'interfaces' => [$PetInterfaceType],
'fields' => [
'name' => ['type' => stringType()],
'woofs' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return $obj instanceof Dog;
}
]);
$CatType = newObjectType([
'name' => 'Cat',
'interfaces' => [$PetInterfaceType],
'fields' => [
'name' => ['type' => stringType()],
'meows' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return $obj instanceof Cat;
}
]);
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => newList($PetInterfaceType),
'resolve' => function ($source, $args, $context, $info) {
return [
new Dog('Odie', true),
new Cat('Garfield', false)
];
}
]
],
]),
'types' => [$DogType, $CatType],
]);
$source = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
/** @var ExecutionResult $executionResult */
$result = execute($schema, parse($source));
$expected = new ExecutionResult([
'pets' => [
[
'name' => 'Odie',
'woofs' => true,
],
[
'name' => 'Garfield',
'meows' => false,
]
]
], []);
$this->assertEquals($expected, $result);
}
/**
* isTypeOf used to resolve runtime type for Union
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testIsTypeOfUsedToResolveRuntimeTypeForUnion()
{
$DogType = newObjectType([
'name' => 'Dog',
'fields' => [
'name' => ['type' => stringType()],
'woofs' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return $obj instanceof Dog;
}
]);
$CatType = newObjectType([
'name' => 'Cat',
'fields' => [
'name' => ['type' => stringType()],
'meows' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return $obj instanceof Cat;
}
]);
$PetUnionType = newUnionType([
'name' => 'Pet',
'types' => [$DogType, $CatType],
'resolveType' => function ($result, $context, $info) use ($DogType, $CatType) {
if ($result instanceof Dog) {
return $DogType;
}
if ($result instanceof Cat) {
return $CatType;
}
return null;
}
]);
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => newList($PetUnionType),
'resolve' => function ($source, $args, $context, $info) {
return [
new Dog('Odie', true),
new Cat('Garfield', false)
];
}
]
]
]),
]);
$source = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
/** @var ExecutionResult $executionResult */
$result = execute($schema, parse($source));
$expected = new ExecutionResult([
'pets' => [
[
'name' => 'Odie',
'woofs' => true,
],
[
'name' => 'Garfield',
'meows' => false,
]
]
], []);
$this->assertEquals($expected->getData(), $result->getData());
}
/**
* resolveType on Interface yields useful error
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testResolveTypeOnInterfaceYieldsUsefulError()
{
$PetInterfaceType = newInterfaceType([
'name' => 'Pet',
'resolveType' => function ($result, $context, $info) use (&$DogType, &$CatType, &$HumanType) {
if ($result instanceof Dog) {
return $DogType;
}
if ($result instanceof Cat) {
return $CatType;
}
if ($result instanceof Human) {
return $HumanType;
}
return null;
}
]);
$DogType = newObjectType([
'name' => 'Dog',
'interfaces' => [$PetInterfaceType],
'fields' => [
'name' => ['type' => stringType()],
'woofs' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return $obj instanceof Dog;
}
]);
$CatType = newObjectType([
'name' => 'Cat',
'interfaces' => [$PetInterfaceType],
'fields' => [
'name' => ['type' => stringType()],
'meows' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return $obj instanceof Cat;
}
]);
$HumanType = newObjectType([
'name' => 'Human',
'fields' => [
'name' => ['type' => stringType()]
],
'isTypeOf' => function ($obj) {
return $obj instanceof Human;
}
]);
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => newList($PetInterfaceType),
'resolve' => function ($source, $args, $context, $info) {
return [
new Dog('Odie', true),
new Cat('Garfield', false),
new Human('John')
];
}
]
]
]),
'types' => [$DogType, $CatType]
]);
$source = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
/** @var ExecutionResult $executionResult */
$result = execute($schema, parse($source));
$expected = new ExecutionResult([
'pets' => [
[
'name' => 'Odie',
'woofs' => true,
],
[
'name' => 'Garfield',
'meows' => false,
],
null
]
], [
new ExecutionException(
'Runtime Object type "Human" is not a possible type for "Pet".',
null,
null,
null,
['pets', 2]
),
]);
$this->assertArraySubset($expected->toArray(), $result->toArray());
}
/**
* resolveType on Union yields useful error
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testResolveTypeOnUnionYieldsUseFulError()
{
$DogType = newObjectType([
'name' => 'Dog',
'fields' => [
'name' => ['type' => stringType()],
'woofs' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return $obj instanceof Dog;
}
]);
$CatType = newObjectType([
'name' => 'Cat',
'fields' => [
'name' => ['type' => stringType()],
'meows' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return $obj instanceof Cat;
}
]);
$HumanType = newObjectType([
'name' => 'Human',
'fields' => [
'name' => ['type' => stringType()]
],
'isTypeOf' => function ($obj) {
return $obj instanceof Human;
}
]);
$PetUnionType = newUnionType([
'name' => 'Pet',
'types' => [$DogType, $CatType],
'resolveType' => function ($result, $context, $info) use ($DogType, $CatType, $HumanType) {
if ($result instanceof Dog) {
return $DogType;
}
if ($result instanceof Cat) {
return $CatType;
}
if ($result instanceof Human) {
return $HumanType;
}
return null;
}
]);
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => newList($PetUnionType),
'resolve' => function ($source, $args, $context, $info) {
return [
new Dog('Odie', true),
new Cat('Garfield', false),
new Human('John')
];
}
]
]
]),
]);
$source = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
/** @var ExecutionResult $executionResult */
$result = execute($schema, parse($source));
$expected = new ExecutionResult([
'pets' => [
[
'name' => 'Odie',
'woofs' => true,
],
[
'name' => 'Garfield',
'meows' => false,
],
null
]
], [
new ExecutionException(
'Runtime Object type "Human" is not a possible type for "Pet".',
null,
null,
null,
['pets', 2]
),
]);
$this->assertArraySubset($expected->toArray(), $result->toArray());
}
/**
* resolveType allows resolving with type name
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testResolveTypeAllowsResolvingWithTypeName()
{
$PetInterfaceType = newInterfaceType([
'name' => 'Pet',
'resolveType' => function ($result, $context, $info) {
if ($result instanceof Dog) {
return 'Dog';
}
if ($result instanceof Cat) {
return 'Cat';
}
return null;
}
]);
$DogType = newObjectType([
'name' => 'Dog',
'interfaces' => [$PetInterfaceType],
'fields' => [
'name' => ['type' => stringType()],
'woofs' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return $obj instanceof Dog;
}
]);
$CatType = newObjectType([
'name' => 'Cat',
'interfaces' => [$PetInterfaceType],
'fields' => [
'name' => ['type' => stringType()],
'meows' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return $obj instanceof Cat;
}
]);
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => newList($PetInterfaceType),
'resolve' => function ($source, $args, $context, $info) {
return [
new Dog('Odie', true),
new Cat('Garfield', false)
];
}
]
]
]),
'types' => [$DogType, $CatType]
]);
$source = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
/** @var ExecutionResult $executionResult */
$result = execute($schema, parse($source));
$expected = new ExecutionResult([
'pets' => [
[
'name' => 'Odie',
'woofs' => true,
],
[
'name' => 'Garfield',
'meows' => false,
]
]
], []);
$this->assertEquals($expected, $result);
}
}
================================================
FILE: tests/Functional/Execution/DeferredResolverTest.php
================================================
[
'name' => 'George Lucas',
],
43 => [
'name' => 'Irvin Kershner'
]
];
}
}
class DeferredResolverTest extends ResolveTest
{
/**
* @throws \Digia\GraphQL\Error\InvariantException
*/
public function testUsingFieldDeferredResolver()
{
$movies = [
[
'title' => 'Episode IV – A New Hope',
'directorId' => 42
],
[
'title' => 'Episode V – The Empire Strikes Back',
'directorId' => 43
]
];
$directorType = newObjectType([
'name' => 'Director',
'description' => 'Director of the movie',
'fields' => [
'name' => [
'type' => stringType(),
]
]
]);
$movieType = newObjectType([
'name' => 'Movie',
'description' => 'A movie',
'fields' => [
'title' => ['type' => stringType()],
'director' => [
'type' => $directorType,
'resolve' => function ($movie, $args) {
DirectorBuffer::add($movie['directorId']);
return new Promise(function (callable $resolve, callable $reject) use ($movie) {
DirectorBuffer::loadBuffered();
$resolve(DirectorBuffer::get($movie['directorId']));
});
}
]
]
]);
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'description' => '',
'fields' => [
'movies' => [
'type' => newList($movieType),
'resolve' => function ($source, $args) use ($movies) {
return $movies;
}
]
]
])
]);
$query = '
{
movies {
title
director {
name
}
}
}
';
$result = graphql($schema, $query, $movies);
$this->assertEquals([
'data' => [
'movies' => [
[
'title' => 'Episode IV – A New Hope',
'director' => [
'name' => 'George Lucas'
]
],
[
'title' => 'Episode V – The Empire Strikes Back',
'director' => [
'name' => 'Irvin Kershner'
]
]
]
]
], $result);
}
}
================================================
FILE: tests/Functional/Execution/DirectivesTest.php
================================================
'a',
'b' => 'b'
];
public function setUp()
{
parent::setUp();
$this->schema = newSchema([
'query' => newObjectType([
'name' => 'TestType',
'fields' => [
'a' => ['type' => stringType()],
'b' => ['type' => stringType()]
]
])
]);
}
// WORKS WITHOUT DIRECTIVES
/**
* works without directives
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testWorksWithDirective()
{
$result = execute($this->schema, parse('{ a, b }'), $this->data);
$this->assertEquals($this->data, $result->getData());
}
/**
* if true includes scalar
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testIfTrueReturnScalar()
{
$result = execute($this->schema, parse('{ a, b @include(if: true) }'), $this->data);
$this->assertEquals($this->data, $result->getData());
}
/**
* if false omits on scalar
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testIfFalseOmitScalar()
{
$result = execute($this->schema, parse('{ a, b @include(if: false) }'), $this->data);
$this->assertSame(['a' => 'a'], $result->getData());
}
/**
* unless false includes scalar
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testUnlessFalseIncludesScalar()
{
$result = execute($this->schema, parse('{ a, b @skip(if: false) }'), $this->data);
$this->assertEquals($this->data, $result->getData());
}
/**
* unless true omits scalar
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testUnlessTrueOmitScalar()
{
$result = execute($this->schema, parse('{ a, b @skip(if: true) }'), $this->data);
$this->assertSame(['a' => 'a'], $result->getData());
}
// WORKS ON FRAGMENT SPREADS
/**
* if false omits fragment spread
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testIfFalseOmitsFragmentSpread()
{
$source = 'query {
a
...Frag @include(if: false)
}
fragment Frag on TestType {
b
}';
$result = execute($this->schema, parse($source), $this->data);
$this->assertSame(['a' => 'a'], $result->getData());
}
/**
* if true includes fragment spread
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testIfTrueIncludesFragmentSpread()
{
$source = 'query {
a
...Frag @include(if: true)
}
fragment Frag on TestType {
b
}';
$result = execute($this->schema, parse($source), $this->data);
$this->assertEquals($this->data, $result->getData());
}
/**
* unless false includes fragment spread
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testUnlessFalseIncludesFragmentSpread()
{
$source = 'query {
a
...Frag @skip(if: false)
}
fragment Frag on TestType {
b
}';
$result = execute($this->schema, parse($source), $this->data);
$this->assertEquals($this->data, $result->getData());
}
/**
* unless true omits fragment spread
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testUnlessTrueOmitsFragmentSpread()
{
$source = 'query {
a
...Frag @skip(if: true)
}
fragment Frag on TestType {
b
}';
$result = execute($this->schema, parse($source), $this->data);
$this->assertSame(['a' => 'a'], $result->getData());
}
// WORKS ON INLINE FRAGMENT
/**
* if false omits inline fragment
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testIfFalseOmitsInlineFragment()
{
$source = 'query {
a
... on TestType @include(if: false) {
b
}
}';
$result = execute($this->schema, parse($source), $this->data);
$this->assertSame(['a' => 'a'], $result->getData());
}
/**
* if true includes inline fragment
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testIfTrueIncludesInlineFragment()
{
$source = 'query {
a
... on TestType @include(if: true) {
b
}
}';
$result = execute($this->schema, parse($source), $this->data);
$this->assertEquals($this->data, $result->getData());
}
/**
* unless false includes inline fragment
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testUnlessFalseIncludesInlineFragment()
{
$source = 'query {
a
... on TestType @skip(if: false) {
b
}
}';
$result = execute($this->schema, parse($source), $this->data);
$this->assertEquals($this->data, $result->getData());
}
/**
* unless true includes inline fragment
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testUnlessTrueIncludesInlineFragments()
{
$source = 'query {
a
... on TestType @skip(if: true) {
b
}
}';
$result = execute($this->schema, parse($source), $this->data);
$this->assertSame(['a' => 'a'], $result->getData());
}
// WORKS ON ANONYMOUS INLINE FRAGMENT
/**
* if false omits anonymous inline fragment
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testIfFalseOmitsAnonymousInlineFragment()
{
$source = 'query {
a
... @include(if: false) {
b
}
}';
$result = execute($this->schema, parse($source), $this->data);
$this->assertSame(['a' => 'a'], $result->getData());
}
/**
* if true includes anonymous inline fragment
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testIfTrueIncludeAnonymousInlineFragment()
{
$source = 'query {
a
... @include(if: true) {
b
}
}';
$result = execute($this->schema, parse($source), $this->data);
$this->assertEquals($this->data, $result->getData());
}
/**
* unless false includes anonymous inline fragment
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testUnlessFalseIncludesAnonymousInlineFragment()
{
$source = 'query Q {
a
... @skip(if: false) {
b
}
}';
$result = execute($this->schema, parse($source), $this->data);
$this->assertEquals($this->data, $result->getData());
}
/**
* unless true includes anonymous inline fragment
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testUnlessTrueIncludeAnonymousInlineFragment()
{
$source = 'query {
a
... @skip(if: true) {
b
}
}';
$result = execute($this->schema, parse($source), $this->data);
$this->assertSame(['a' => 'a'], $result->getData());
}
// WORKS WITH SKIP AND INCLUDE DIRECTIVES
/**
* include and no skip
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testIncludeAndNoSkip()
{
$source = '{
a
b @include(if: true) @skip(if: false)
}';
$result = execute($this->schema, parse($source), $this->data);
$this->assertEquals($this->data, $result->getData());
}
/**
* include and skip
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testIncludeAndSkip()
{
$source = '{
a
b @include(if: true) @skip(if: true)
}';
$result = execute($this->schema, parse($source), $this->data);
$this->assertSame(['a' => 'a'], $result->getData());
}
/**
* no include or skip
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testNoIncludeOrSkip()
{
$source = '{
a
b @include(if: false) @skip(if: false)
}';
$result = execute($this->schema, parse($source), $this->data);
$this->assertSame(['a' => 'a'], $result->getData());
}
}
================================================
FILE: tests/Functional/Execution/ExecutionTest.php
================================================
expectException(\TypeError::class);
$schema = newSchema([
'query' => newObjectType([
'name' => 'Type',
'fields' => [
'a' => [
'type' => stringType()
]
]
])
]);
graphql($schema, null);
}
/**
* throws if no schema is provided
*
* @throws \Exception
*/
public function testNoSchemaIsProvided()
{
$this->expectException(\TypeError::class);
graphql(null, '{field}');
}
/**
* Test accepts an object with named properties as arguments
* @throws \Digia\GraphQL\Error\InvariantException
*/
public function testAcceptAnObjectWithNamedPropertiesAsArguments()
{
$schema = newSchema([
'query' => newObjectType([
'name' => 'Greeting',
'fields' => [
'a' => [
'type' => stringType(),
'resolve' => function ($source, $args, $context, $info) {
return $source;
}
]
]
])
]);
$rootValue = 'rootValue';
$source = 'query Example { a }';
/** @var ExecutionResult $executionResult */
$result = graphql($schema, $source, $rootValue);
$this->assertSame(['data' => ['a' => $rootValue]], $result);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
*/
public function testExecuteHelloQuery()
{
$schema = newSchema([
'query' => newObjectType([
'name' => 'Greeting',
'fields' => [
'hello' => [
'type' => stringType(),
'resolve' => function () {
return 'world';
}
]
]
])
]);
/** @var ExecutionResult $executionResult */
$executionResult = graphql($schema, 'query Greeting {hello}');
$this->assertSame(['data' => ['hello' => 'world']], $executionResult);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Language\SyntaxErrorException
*/
public function testExecuteArbitraryCode()
{
$deep = newObjectType([
'name' => 'DeepDataType',
'fields' => function () use (&$dataType) {
return [
'a' => [
'type' => stringType(),
'resolve' => function () {
return 'Already Been Done';
}
],
'b' => [
'type' => stringType(),
'resolve' => function () {
return 'Boring';
}
],
'c' => [
'type' => newList(stringType()),
'resolve' => function () {
return ['Contrived', null, 'Confusing'];
}
],
'deeper' => [
'type' => newList($dataType),
'resolve' => function () {
return [
[
'a' => 'Apple',
'b' => 'Banana',
],
null
,
[
'a' => 'Apple',
'b' => 'Banana',
]
];
}
]
];
}
]);
$dataType = newObjectType([
'name' => 'DataType',
'fields' => function () use (&$dataType, &$deep) {
return [
'a' => [
'type' => stringType(),
'resolve' => function () {
return 'Apple';
}
],
'b' => [
'type' => stringType(),
'resolve' => function () {
return 'Banana';
}
],
'c' => [
'type' => stringType(),
'resolve' => function () {
return 'Cookie';
}
],
'd' => [
'type' => stringType(),
'resolve' => function () {
return 'Donut';
}
],
'e' => [
'type' => stringType(),
'resolve' => function () {
return 'Egg';
}
],
'f' => [
'type' => stringType(),
'resolve' => function () {
return 'Fish';
}
],
'pic' => [
'type' => stringType(),
'resolve' => function ($src, $args) {
return 'Pic of size: ' . ($args['size'] ?? 50);
},
'args' => [
'size' => [
'type' => intType(),
]
],
],
'promise' => [
'type' => $dataType,
'resolve' => function () {
return [];
}
],
'deep' => [
'type' => $deep,
'resolve' => function () {
return [];
}
]
];
}
]);
$source = '
query Example($size: Int) {
a,
b,
x: c
...c
f
...on DataType {
pic(size: $size)
promise {
a
}
}
deep {
a
b
c
deeper {
a
b
}
}
}
fragment c on DataType {
d
e
}
';
$schema = newSchema(['query' => $dataType]);
/** @var ExecutionResult $executionResult */
$executionResult = execute($schema, parse($source), null, null, ['size' => 100]);
$expected = new ExecutionResult([
'a' => 'Apple',
'b' => 'Banana',
'x' => 'Cookie',
'd' => 'Donut',
'e' => 'Egg',
'f' => 'Fish',
'pic' => 'Pic of size: 100',
'promise' => [
'a' => 'Apple'
],
'deep' => [
'a' => 'Already Been Done',
'b' => 'Boring',
'c' => ['Contrived', null, 'Confusing'],
'deeper' => [
['a' => 'Apple', 'b' => 'Banana'],
null,
['a' => 'Apple', 'b' => 'Banana'],
]
]
], []);
$this->assertEquals($expected, $executionResult);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
*/
public function testExecuteQueryHelloWithArgs()
{
$schema = newSchema([
'query' => newObjectType([
'name' => 'Greeting',
'fields' => [
'greeting' => [
'type' => stringType(),
'resolve' => function ($source, $args, $context, $info) {
return sprintf('Hello %s', $args['name']);
},
'args' => [
'name' => [
'type' => stringType(),
]
]
]
]
])
]);
$source = 'query Hello($name: String) {greeting(name: $name)}';
$variableValues = ['name' => 'Han Solo'];
/** @var ExecutionResult $executionResult */
$executionResult = graphql($schema, $source, '', null, $variableValues);
$this->assertSame(['data' => ['greeting' => 'Hello Han Solo']], $executionResult);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
*/
public function testExecuteQueryWithMultipleFields()
{
$schema = newSchema([
'query' => newObjectType([
'name' => 'Human',
'fields' => [
'id' => [
'type' => intType(),
'resolve' => function () {
return 1000;
}
],
'type' => [
'type' => stringType(),
'resolve' => function () {
return 'Human';
}
],
'friends' => [
'type' => newList(stringType()),
'resolve' => function () {
return ['1002', '1003', '2000', '2001'];
}
],
'appearsIn' => [
'type' => newList(intType()),
'resolve' => function () {
return [4, 5, 6];
}
],
'homePlanet' => [
'type' => stringType(),
'resolve' => function () {
return 'Tatooine';
}
],
],
]),
]);
/** @var ExecutionResult $executionResult */
$executionResult = graphql($schema, 'query Human {id, type, friends, appearsIn, homePlanet}');
$this->assertSame([
'data' => [
'id' => 1000,
'type' => 'Human',
'friends' => ['1002', '1003', '2000', '2001'],
'appearsIn' => [4, 5, 6],
'homePlanet' => 'Tatooine'
]
], $executionResult);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
*/
public function testMergeParallelFragments()
{
$source = '
{ a, ...FragOne, ...FragTwo }
fragment FragOne on Type {
b
deep { b, deeper: deep { b } }
}
fragment FragTwo on Type {
c
deep { c, deeper: deep { c } }
}
';
$type = newObjectType([
'name' => 'Type',
'fields' => function () use (&$type) {
return [
'a' => [
'type' => stringType(),
'resolve' => function () {
return 'Apple';
}
],
'b' => [
'type' => stringType(),
'resolve' => function () {
return 'Banana';
}
],
'c' => [
'type' => stringType(),
'resolve' => function () {
return 'Cherry';
}
],
'deep' => [
'type' => $type,
'resolve' => function () {
return [];
}
]
];
}
]);
$schema = newSchema(['query' => $type]);
/** @var ExecutionResult $executionResult */
$executionResult = graphql($schema, $source);
$this->assertSame([
'data' => [
'a' => 'Apple',
'b' => 'Banana',
'deep' => [
'b' => 'Banana',
'deeper' => [
'b' => 'Banana',
'c' => 'Cherry'
],
'c' => 'Cherry',
],
'c' => 'Cherry'
]
], $executionResult);
}
/**
* provides info about current execution state
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Language\SyntaxErrorException
*/
public function testProvidesInfoAboutCurrentExecutionState()
{
$schema = newSchema([
'query' => newObjectType([
'name' => 'Test',
'fields' => [
'test' => [
'type' => stringType(),
'resolve' => function ($source, $args, $context, $_info) use (&$info) {
$info = $_info;
}
]
]
]),
]);
$rootValue = [
'rootValue' => 'val'
];
$ast = parse('query ($var: String) { result: test }');
execute($schema, $ast, $rootValue, null, ['var' => 123]);
/** @var ResolveInfo $info */
$this->assertSame('test', $info->getFieldName());
$this->assertSame(1, count($info->getFieldNodes()));
$this->assertEquals(
$ast->getDefinitions()[0]->getSelectionSet()->getSelections()[0],
$info->getFieldNodes()[0]
);
$this->assertSame(stringType(), $info->getReturnType());
$this->assertEquals($schema->getQueryType(), $info->getParentType());
$this->assertSame(["result"], $info->getPath()); // { prev: undefined, key: 'result' }
$this->assertEquals($schema, $info->getSchema());
$this->assertEquals($rootValue, $info->getRootValue());
$this->assertEquals($ast->getDefinitions()[0], $info->getOperation());
$this->assertEquals(['var' => 123], $info->getVariableValues());
}
/**
* Threads root value context correctly
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Language\SyntaxErrorException
*/
public function testThreadsRootValueContextCorrectly()
{
$resolvedRootValue = null;
$schema = newSchema([
'query' => newObjectType([
'name' => 'Test',
'fields' => [
'a' => [
'type' => stringType(),
'resolve' => function ($rootValue) use (&$resolvedRootValue) {
$resolvedRootValue = $rootValue;
}
]
]
]),
]);
$data = ['contextThing' => 'thing'];
$ast = parse('query Example { a }');
execute($schema, $ast, $data);
$this->assertEquals('thing', $resolvedRootValue['contextThing']);
}
/**
* Correctly threads arguments
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Language\SyntaxErrorException
*/
public function testCorrectlyThreadsArguments()
{
$resolvedArgs = null;
$schema = newSchema([
'query' => newObjectType([
'name' => 'Type',
'fields' => [
'b' => [
'type' => intType(),
'args' => [
'numArg' => ['type' => intType()],
'stringArg' => ['type' => stringType()]
],
'resolve' => function ($source, $args) use (&$resolvedArgs) {
$resolvedArgs = $args;
}
]
]
]),
]);
execute($schema, parse('query Example { b(numArg: 123, stringArg: "foo") }'));
$this->assertEquals(['numArg' => 123, 'stringArg' => 'foo'], $resolvedArgs);
}
public function testNullsOutErrorsSubTrees()
{
$source = dedent('{
sync
syncError
syncRawError
syncReturnError
syncReturnErrorList
async
asyncReject
asyncRawReject
asyncEmptyReject
asyncError
asyncRawError
asyncReturnError
}');
$data = [
'sync' => function () {
return 'sync';
},
'syncError' => function () {
throw new \Exception('Error getting syncError');
},
'syncRawError' => function () {
throw new \Exception('Error getting syncRawError');
},
'syncReturnError' => function () {
return new \Exception('Error getting syncReturnError');
},
'syncReturnErrorList' => function () {
return [
'sync0',
new \Exception('Error getting syncReturnErrorList1'),
'sync2',
new \Exception('Error getting syncReturnErrorList3'),
];
},
'async' => function () {
return \React\Promise\resolve('async');
},
'asyncReject' => function () {
return new \React\Promise\Promise(function ($resolve, $reject) {
$reject(new \Exception('Error getting asyncReject'));
});
},
'asyncRawReject' => function () {
return \React\Promise\reject('Error getting asyncRawReject');
},
'asyncEmptyReject' => function () {
return \React\Promise\reject(null);
},
'asyncError' => function () {
return new \React\Promise\Promise(function () {
throw new \Exception('Error getting asyncError');
});
},
'asyncRawError' => function () {
return new \React\Promise\Promise(function () {
throw new \Exception('Error getting asyncRawError');
});
},
'asyncReturnError' => function () {
return \React\Promise\resolve(new \Exception('Error getting asyncReturnError'));
},
];
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Type',
'fields' => [
'sync' => ['type' => stringType()],
'syncError' => ['type' => stringType()],
'syncRawError' => ['type' => stringType()],
'syncReturnError' => ['type' => stringType()],
'syncReturnErrorList' => ['type' => newList(stringType())],
'async' => ['type' => stringType()],
'asyncReject' => ['type' => stringType()],
'asyncRawReject' => ['type' => stringType()],
'asyncEmptyReject' => ['type' => stringType()],
'asyncError' => ['type' => stringType()],
'asyncRawError' => ['type' => stringType()],
'asyncReturnError' => ['type' => stringType()],
]
]),
]);
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($source), $data);
$this->assertEquals([
'errors' => [
[
'message' => 'Error getting syncError',
'locations' => [
[
'line' => 3,
'column' => 11
]
],
'path' => ['syncError']
],
[
'message' => 'Error getting syncRawError',
'locations' => [
[
'line' => 4,
'column' => 11
]
],
'path' => ['syncRawError']
],
[
'message' => 'Error getting syncReturnError',
'locations' => [
[
'line' => 5,
'column' => 11
]
],
'path' => ['syncReturnError']
],
[
'message' => 'Error getting syncReturnErrorList1',
'locations' => [
[
'line' => 6,
'column' => 11
]
],
'path' => ['syncReturnErrorList', 1]
],
[
'message' => 'Error getting syncReturnErrorList3',
'locations' => [
[
'line' => 6,
'column' => 11
]
],
'path' => ['syncReturnErrorList', 3]
],
[
'message' => 'Error getting asyncReject',
'locations' => [
[
'line' => 8,
'column' => 11
]
],
'path' => ['asyncReject']
],
[
'message' => 'Error getting asyncRawReject',
'locations' => [
[
'line' => 9,
'column' => 11,
]
],
'path' => ['asyncRawReject']
],
[
'message' => '',
'locations' => [
[
'line' => 10,
'column' => 11,
]
],
'path' => ['asyncEmptyReject']
],
[
'message' => 'Error getting asyncError',
'locations' => [
[
'line' => 11,
'column' => 11
]
],
'path' => ['asyncError']
],
[
'message' => 'Error getting asyncRawError',
'locations' => [
[
'line' => 12,
'column' => 11
]
],
'path' => ['asyncRawError']
],
[
'message' => 'Error getting asyncReturnError',
'locations' => [
[
'line' => 13,
'column' => 11
]
],
'path' => ['asyncReturnError']
],
],
'data' => [
'sync' => 'sync',
'syncError' => null,
'syncRawError' => null,
'syncReturnError' => null,
'syncReturnErrorList' => ['sync0', null, 'sync2', null],
'async' => 'async',
'asyncReject' => null,
'asyncRawReject' => null,
'asyncEmptyReject' => null,
'asyncError' => null,
'asyncRawError' => null,
'asyncReturnError' => null,
],
], $result->toArray());
}
public function testNullsErrorSubTreeForPromiseRejection()
{
$query = '
query {
foods {
name
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Type',
'fields' => [
'foods' => [
'type' => newList(newObjectType([
'name' => 'Food',
'fields' => [
'name' => [
'type' => stringType()
]
]
])),
'resolve' => function () {
return \React\Promise\reject(new \Exception('Dangit'));
}
],
]
]),
]);
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($query));
$this->assertEquals([
'data' => [
'foods' => null
],
'errors' => [
[
'message' => 'Dangit',
'locations' => [
[
'line' => 3,
'column' => 13
]
],
'path' => ['foods']
]
]
], $result->toArray());
}
public function testFullResponsePathIsIncludedForNonNullableFields()
{
/** @noinspection PhpUnhandledExceptionInspection */
$A = newObjectType([
'name' => 'A',
'fields' => function () use (&$A) {
return [
'nullableA' => [
'type' => $A,
'resolve' => function () {
return [];
},
],
'nonNullA' => [
'type' => newNonNull($A),
'resolve' => function () {
return [];
},
],
'throws' => [
'type' => newNonNull(stringType()),
'resolve' => function () {
throw new \Exception('Catch me if you can!');
},
],
];
},
]);
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'query',
'fields' => function () use (&$A) {
return [
'nullableA' => [
'type' => $A,
'resolve' => function () {
return [];
},
],
];
},
])
]);
$query = dedent('
query {
nullableA {
aliasedA: nullableA {
nonNullA {
anotherA: nonNullA {
throws
}
}
}
}
}
');
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($query));
$this->assertSame([
'errors' => [
[
'message' => 'Catch me if you can!',
'locations' => [
[
'line' => 6,
'column' => 1,
],
],
'path' => [
'nullableA',
'aliasedA',
'nonNullA',
'anotherA',
'throws',
]
]
],
'data' => [
'nullableA' => [
'aliasedA' => null,
],
],
], $result->toArray());
}
/**
* Uses the inline operation if no operation name is provided
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Language\SyntaxErrorException
*/
public function testUsesTheInlineOperationIfNoOperationIsProvided()
{
$rootValue = ['a' => 'b'];
$schema = newSchema([
'query' => newObjectType([
'name' => 'Type',
'fields' => [
'a' => [
'type' => stringType(),
]
]
])
]);
$result = execute($schema, parse('{ a }'), $rootValue);
$this->assertEquals([
'data' => [
'a' => 'b',
]
], $result->toArray());
}
/**
* Uses the named operation if operation name is provided
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Language\SyntaxErrorException
*/
public function testUsesTheNamedOperationIfOperationNameIsProvided()
{
$rootValue = ['a' => 'b'];
$schema = newSchema([
'query' => newObjectType([
'name' => 'Type',
'fields' => [
'a' => [
'type' => stringType(),
]
]
])
]);
$query = 'query Example { first: a } query OtherExample { second: a }';
$result = execute($schema, parse($query), $rootValue, null, [], 'OtherExample');
$this->assertEquals([
'data' => [
'second' => 'b',
]
], $result->toArray());
}
/**
* Provides error if no operation is provided
*/
public function testProvidesErrorIfNoOperationIsProvided()
{
$rootValue = ['a' => 'b'];
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Type',
'fields' => [
'a' => [
'type' => stringType(),
]
]
])
]);
$query = 'fragment Example on Type { a }';
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($query), $rootValue);
$this->assertEquals([
'data' => null,
'errors' => [
[
'message' => 'Must provide an operation.',
'locations' => null,
]
]
], $result->toArray());
}
/**
* Errors if no op name is provided with multiple operations
*/
public function testErrorsIfNoOpNameIsProvidedWithMultipleOperations()
{
$rootValue = ['a' => 'b'];
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Type',
'fields' => [
'a' => [
'type' => stringType(),
]
]
])
]);
$query = 'query Example { a } query OtherExample { a }';
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($query), $rootValue);
$this->assertEquals([
'data' => null,
'errors' => [
[
'message' => 'Must provide operation name if query contains multiple operations.',
'locations' => null,
]
]
], $result->toArray());
}
/**
* Errors if unknown operation name is provided
*/
public function testErrorsIfUnknownOperationNameIsProvided()
{
$rootValue = ['a' => 'b'];
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Type',
'fields' => [
'a' => [
'type' => stringType(),
]
]
])
]);
$query = 'query Example { a } query OtherExample { a }';
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($query), $rootValue, [], [], 'UnknownExample');
$this->assertEquals([
'data' => null,
'errors' => [
[
'message' => 'Unknown operation named "UnknownExample".',
'locations' => null,
]
]
], $result->toArray());
}
/**
* Uses the query schema for queries
*/
public function testUsesTheQuerySchemaForQueries()
{
$rootValue = ['a' => 'b'];
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Q',
'fields' => [
'a' => [
'type' => stringType(),
]
]
]),
'mutation' => newObjectType([
'name' => 'M',
'fields' => [
'c' => [
'type' => stringType(),
]
]
]),
'subscription' => newObjectType([
'name' => 'S',
'fields' => [
'a' => [
'type' => stringType(),
]
]
])
]);
$query = 'query Q { a } mutation M { c } subscription S { a }';
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($query), $rootValue, [], [], 'Q');
$this->assertEquals([
'data' => [
'a' => 'b',
]
], $result->toArray());
}
/**
* Uses the mutation schema for mutations
*/
public function testUsesTheMutationSchemaForMutations()
{
$rootValue = ['a' => 'b', 'c' => 'd'];
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Q',
'fields' => [
'a' => [
'type' => stringType(),
],
],
]),
'mutation' => newObjectType([
'name' => 'M',
'fields' => [
'c' => [
'type' => stringType(),
],
],
]),
]);
$query = 'query Q { a } mutation M { c } subscription S { a }';
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($query), $rootValue, [], [], 'M');
$this->assertEquals([
'data' => [
'c' => 'd',
]
], $result->toArray());
}
/**
* Uses the subscription schema for subscriptions
*/
public function testUsesTheSubscriptionSchemaForSubscriptions()
{
$rootValue = ['a' => 'b'];
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Q',
'fields' => [
'a' => [
'type' => stringType(),
]
]
]),
'subscription' => newObjectType([
'name' => 'S',
'fields' => [
'a' => [
'type' => stringType(),
]
]
])
]);
$query = 'query Q { a } subscription S { a }';
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($query), $rootValue, [], [], 'S');
$this->assertEquals([
'data' => [
'a' => 'b',
]
], $result->toArray());
}
/**
* Correct field ordering despite execution order
*/
public function testCorrectFieldOrderingDespiteExecutionOrder()
{
$rootValue = [
'a' => function () {
return 'a';
},
'b' => function () {
return \React\Promise\resolve('b');
},
'c' => function () {
return 'c';
},
'd' => function () {
return \React\Promise\resolve('d');
},
'e' => function () {
return 'e';
},
];
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Q',
'fields' => [
'a' => ['type' => stringType()],
'b' => ['type' => stringType()],
'c' => ['type' => stringType()],
'd' => ['type' => stringType()],
'e' => ['type' => stringType()],
],
]),
]);
$query = '{ a, b, c, d, e }';
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($query), $rootValue);
$this->assertEquals([
'data' => [
'a' => 'a',
'b' => 'b',
'c' => 'c',
'd' => 'd',
'e' => 'e',
],
], $result->toArray());
}
/**
* Avoid recursions
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Language\SyntaxErrorException
*/
public function testAvoidRecursions()
{
$rootValue = ['a' => 'b'];
$schema = newSchema([
'query' => newObjectType([
'name' => 'Type',
'fields' => [
'a' => [
'type' => stringType(),
],
],
]),
]);
$query = '
query Q {
a
...Frag
...Frag
}
fragment Frag on Type {
a
...Frag
}
';
$result = execute($schema, parse($query), $rootValue, [], [], 'Q');
$this->assertEquals([
'data' => [
'a' => 'b',
],
], $result->toArray());
}
/**
* Does not include illegal fields in output
*/
public function testDoesNotIncludeIllegalFieldsInOutput()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Q',
'fields' => [
'a' => ['type' => stringType()],
],
]),
'mutation' => newObjectType([
'name' => 'M',
'fields' => [
'c' => ['type' => stringType()],
],
]),
]);
/** @noinspection PhpUnhandledExceptionInspection */
$executionResult = execute($schema, parse('mutation M { thisIsIllegalDontIncludeMe }'));
$expected = new ExecutionResult([], []);
$this->assertEquals($expected, $executionResult);
}
/**
* Fails when an isTypeOf check is not met
*/
public function testFailsWhenAnIsTypeOfCheckIsNotMet()
{
/** @noinspection PhpUnhandledExceptionInspection */
$specialType = newObjectType([
'name' => 'specialType',
'fields' => [
'value' => [
'type' => stringType()
]
],
'isTypeOf' => function ($obj) {
return $obj instanceof Special;
},
]);
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => function () use ($specialType) {
return [
'specials' => [
'type' => newList($specialType),
'resolve' => function ($root) {
return $root['specials'];
}
]
];
}
])
]);
$rootValue = [
'specials' => [
new Special('foo'),
new NotSpecial('bar')
]
];
$query = '{ specials { value } }';
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($query), $rootValue);
$this->assertSame([
'errors' => [
[
'message' => 'Expected value of type "specialType" but got: Object.',
'locations' => [
[
'line' => 1,
'column' => 3,
],
],
'path' => ['specials', 1],
],
],
'data' => [
'specials' => [
['value' => 'foo'],
null,
],
],
], $result->toArray());
}
/**
* Executes ignoring invalid non-executable definitions
*/
public function testExecutesIgnoringInvalidNonExecutableDefinitions()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'foo' => [
'type' => stringType(),
]
]
])
]);
$query = '{ foo } type Query { bar: String }';
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($query));
$this->assertEquals([
'data' => [
'foo' => null,
]
], $result->toArray());
}
/**
* Uses a custom field resolver
*/
public function testUsesACustomFieldResolver()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'foo' => [
'type' => stringType(),
]
]
])
]);
$query = '{ foo }';
$customResolver = function ($source, $args, $context, ResolveInfo $info) {
return $info->getFieldName();
};
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($query), [], [], [], null, $customResolver);
$this->assertEquals([
'data' => [
'foo' => 'foo',
]
], $result->toArray());
}
public function testNonNullFieldQuery()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Greeting',
'fields' => [
'hello' => [
'type' => newNonNull(stringType()),
'resolve' => function () {
return null;
}
]
]
])
]);
/** @noinspection PhpUnhandledExceptionInspection */
$executionResult = graphql($schema, 'query Greeting {hello}');
$this->assertSame([
'errors' => [
[
'message' => 'Cannot return null for non-nullable field Greeting.hello.',
'locations' => null,
'path' => ['hello'],
]
],
'data' => null
], $executionResult);
}
}
class Special
{
public $value;
public function __construct($value)
{
$this->value = $value;
}
}
class NotSpecial
{
public $value;
public function __construct($value)
{
$this->value = $value;
}
}
================================================
FILE: tests/Functional/Execution/ListTest.php
================================================
'DataType',
'fields' => function () use (&$dataType, $testType, $testData) {
return [
'test' => [
'type' => $testType
],
'nest' => [
'type' => $dataType,
'resolve' => function () use ($testData) {
return [
'test' => $testData
];
}
]
];
}
]);
$source = '{ nest { test } }';
$schema = newSchema(['query' => $dataType]);
/** @var ExecutionResult $executionResult */
$result = execute($schema, parse($source));
$this->assertEquals($expected, $result->toArray());
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testAcceptAnArrayObjectAsListValue()
{
$this->makeTest(newList(stringType()),
new \ArrayObject(['apple', 'banana', 'coconut']),
[
'data' => [
'nest' => [
'test' => ['apple', 'banana', 'coconut']
]
]
]
);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testAcceptsAGeneratorAsFunctionValue()
{
$this->makeTest(newList(stringType()),
function () {
yield 'one';
yield 2;
yield true;
},
[
'data' => [
'nest' => [
'test' => ['one', '2', 'true']
]
]
]
);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testDoesNotAcceptStringAsIterable()
{
$this->makeTest(newList(stringType()),
'Singluar',
[
'data' => [
'nest' => [
'test' => null
]
]
,
'errors' => [
[
'message' => 'Expected Array or Traversable, but did not find one for field DataType.test.',
'locations' => [
[
'line' => 1,
'column' => 10
]
],
'path' => ['nest', 'test']
]
]
]
);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testArrayContainsValues()
{
$this->makeTest(newList(intType()),
[1, 2],
[
'data' => [
'nest' => [
'test' => [1, 2]
]
]
]
);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testArrayContainsNull()
{
$this->makeTest(newList(intType()),
[1, null, 2],
[
'data' => [
'nest' => [
'test' => [1, null, 2]
]
]
]
);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testArrayReturnsNull()
{
$this->makeTest(newList(intType()),
null,
[
'data' => [
'nest' => [
'test' => null
]
]
]
);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testPromiseContainsValues()
{
$this->makeTest(newList(intType()),
\React\Promise\resolve([1, 2]),
[
'data' => [
'nest' => [
'test' => [1, 2]
]
]
]
);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testPromiseContainsNull()
{
$this->makeTest(newList(intType()),
\React\Promise\resolve([1, null, 2]),
[
'data' => [
'nest' => [
'test' => [1, null, 2]
]
]
]
);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testPromiseReturnsNull()
{
$this->makeTest(newList(intType()),
\React\Promise\resolve(null),
[
'data' => [
'nest' => [
'test' => null
]
]
]
);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testPromiseReject()
{
$this->makeTest(newList(intType()),
\React\Promise\reject(new \Exception('Bad')),
[
'data' => [
'nest' => [
'test' => null
]
],
'errors' => [
[
'message' => 'Bad',
'locations' => [
[
'line' => 1,
'column' => 10
]
],
'path' => ['nest', 'test']
]
]
]
);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testArrayPromiseContainsValues()
{
$this->makeTest(newList(intType()),
[
\React\Promise\resolve(1),
\React\Promise\resolve(2)
],
[
'data' => [
'nest' => [
'test' => [1, 2]
]
]
]
);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testArrayPromiseContainsNull()
{
$this->makeTest(newList(intType()),
[
\React\Promise\resolve(1),
\React\Promise\resolve(null),
\React\Promise\resolve(2)
],
[
'data' => [
'nest' => [
'test' => [1, null, 2]
]
]
]
);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testArrayPromiseContainsReject()
{
$this->makeTest(newList(intType()),
[
\React\Promise\resolve(1),
\React\Promise\reject(new \Exception('Bad')),
\React\Promise\resolve(2)
],
[
'data' => [
'nest' => [
'test' => [1, null, 2]
]
],
'errors' => [
[
'message' => 'Bad',
'locations' => [
[
'line' => 1,
'column' => 10
]
],
'path' => ['nest', 'test', 1]
]
]
]
);
}
}
================================================
FILE: tests/Functional/Execution/MutationTest.php
================================================
newObjectType([
'name' => 'M',
'fields' => [
'greeting' => [
'type' => stringType(),
'resolve' => function ($source, $args, $context, $info) {
return sprintf('Hello %s.', $args['name']);
},
'args' => [
'name' => [
'type' => stringType()
]
]
]
]
])
]);
$source = '
mutation M($name: String) {
greeting(name:$name)
}';
$executionResult = execute($schema, parse($source), '', null, ['name' => 'Han Solo']);
$expected = new ExecutionResult([
'greeting' => 'Hello Han Solo.'
], []);
$this->assertEquals($expected, $executionResult);
}
// EXECUTE: HANDLES MUTATION EXECUTION ORDERING
/**
* Evaluates mutations serially
*
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testEvaluatesMutationsSerially()
{
$source = 'mutation M {
first: immediatelyChangeTheNumber(newNumber: 1) {
theNumber
},
second: promiseToChangeTheNumber(newNumber: 2) {
theNumber
},
third: immediatelyChangeTheNumber(newNumber: 3) {
theNumber
}
fourth: promiseToChangeTheNumber(newNumber: 4) {
theNumber
},
fifth: immediatelyChangeTheNumber(newNumber: 5) {
theNumber
}
}';
$result = execute(rootSchema(), parse($source), new Root(6));
$expected = [
'data' => [
'first' => [
'theNumber' => 1
],
'second' => [
'theNumber' => 2
],
'third' => [
'theNumber' => 3
],
'fourth' => [
'theNumber' => 4
],
'fifth' => [
'theNumber' => 5
]
]
];
$this->assertEquals($expected, $result->toArray());
}
//evaluates mutations correctly in the presence of a failed mutation
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testEvaluatesMutationsCorrectlyInThePresenceOfAvailedMutation()
{
$source = 'mutation M {
first: immediatelyChangeTheNumber(newNumber: 1) {
theNumber
},
second: promiseToChangeTheNumber(newNumber: 2) {
theNumber
},
third: failToChangeTheNumber(newNumber: 3) {
theNumber
}
fourth: promiseToChangeTheNumber(newNumber: 4) {
theNumber
},
fifth: immediatelyChangeTheNumber(newNumber: 5) {
theNumber
}
sixth: promiseAndFailToChangeTheNumber(newNumber: 6) {
theNumber
}
}';
$result = execute(rootSchema(), parse($source), new Root(6));
$expected = [
'data' => [
'first' => [
'theNumber' => 1
],
'second' => [
'theNumber' => 2
],
'third' => null,
'fourth' => [
'theNumber' => 4
],
'fifth' => [
'theNumber' => 5
],
'sixth' => null
],
'errors' => [
[
'message' => 'Cannot change the number',
'locations' => null,
'path' => ['third']
],
[
'message' => 'Cannot change the number',
'locations' => null,
'path' => ['sixth']
]
]
];
$this->assertEquals($expected, $result->toArray());
}
}
class NumberHolder
{
/**
* @var int
*/
public $theNumber;
/**
* NumberHolder constructor.
* @param int $originalNumber
*/
public function __construct(int $originalNumber)
{
$this->theNumber = $originalNumber;
}
}
class Root
{
public $numberHolder;
/**
* Root constructor.
* @param int $originalNumber
*/
public function __construct(int $originalNumber)
{
$this->numberHolder = new NumberHolder($originalNumber);
}
/**
* @param int $newNumber
* @return NumberHolder
*/
public function immediatelyChangeTheNumber(int $newNumber)
{
$this->numberHolder->theNumber = $newNumber;
return $this->numberHolder;
}
/**
* @param int $newNumber
*
* @return Promise
*/
public function promiseToChangeTheNumber(int $newNumber)
{
return new Promise(function (callable $resolve) use ($newNumber) {
return $resolve($this->immediatelyChangeTheNumber($newNumber));
});
}
/**
* @throws \Exception
*/
public function failToChangeTheNumber()
{
throw new ExecutionException('Cannot change the number');
}
/**
* @return Promise
*/
public function promiseAndFailToChangeTheNumber()
{
return new Promise(function (callable $resolve, callable $reject) {
$reject(new ExecutionException("Cannot change the number"));
});
}
}
function rootSchema(): Schema
{
$numberHolderType = newObjectType([
'fields' => [
'theNumber' => ['type' => intType()],
],
'name' => 'NumberHolder',
]);
$schema = newSchema([
'query' => newObjectType([
'fields' => [
'numberHolder' => ['type' => $numberHolderType],
],
'name' => 'Query',
]),
'mutation' => newObjectType([
'fields' => [
'immediatelyChangeTheNumber' => [
'type' => $numberHolderType,
'args' => ['newNumber' => ['type' => intType()]],
'resolve' => function (Root $obj, $args) {
return $obj->immediatelyChangeTheNumber($args['newNumber']);
}
],
'promiseToChangeTheNumber' => [
'type' => $numberHolderType,
'args' => ['newNumber' => ['type' => intType()]],
'resolve' => function (Root $obj, $args) {
return $obj->promiseToChangeTheNumber($args['newNumber']);
}
],
'failToChangeTheNumber' => [
'type' => $numberHolderType,
'args' => ['newNumber' => ['type' => intType()]],
'resolve' => function (Root $obj) {
return $obj->failToChangeTheNumber();
}
],
'promiseAndFailToChangeTheNumber' => [
'type' => $numberHolderType,
'args' => ['newNumber' => ['type' => intType()]],
'resolve' => function (Root $obj) {
return $obj->promiseAndFailToChangeTheNumber();
}
]
],
'name' => 'Mutation',
])
]);
return $schema;
}
================================================
FILE: tests/Functional/Execution/NonNullTest.php
================================================
schemaWithNonNullArg = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'withNonNullArg' => [
'type' => stringType(),
'args' => [
'cannotBeNull' => [
'type' => newNonNull(stringType()),
],
],
'resolve' => function ($_, $args): ?string {
return isset($args['cannotBeNull']) && \is_string($args['cannotBeNull'])
? "Passed: {$args['cannotBeNull']}"
: null;
}
],
],
])
]);
}
// Handles non-null argument
public function testSucceedsWhenPassedNonNullLiteralValue()
{
$this->assertQueryResult(
'query {
withNonNullArg (cannotBeNull: "literal value")
}',
[
'data' => [
'withNonNullArg' => 'Passed: literal value'
]
]
);
}
public function testSucceedsWhenPassedNonNullVariableValue()
{
$this->assertQueryResult(
'query ($testVar: String!) {
withNonNullArg (cannotBeNull: $testVar)
}',
[
'data' => [
'withNonNullArg' => 'Passed: variable value'
]
],
['testVar' => 'variable value']
);
}
public function testSucceedsWhenMissingVariableHasDefaultValue()
{
$this->assertQueryResult(
'query ($testVar: String = "default value") {
withNonNullArg (cannotBeNull: $testVar)
}',
[
'data' => [
'withNonNullArg' => 'Passed: default value'
]
],
// Intentionally missing variable
[]
);
}
public function testFieldErrorWhenMissingNonNullArg()
{
// Note: validation should identify this issue first (missing args rule)
// however execution should still protect against this.
$this->assertQueryResult(
'query {
withNonNullArg
}',
[
'data' => [
'withNonNullArg' => null,
],
'errors' => [
[
'message' => 'Argument "cannotBeNull" of required type "String!" was not provided.',
'locations' => [
[
'line' => 2,
'column' => 15,
],
],
'path' => ['withNonNullArg'],
],
],
]
);
}
public function testFieldErrorWhenNonNullArgProvidedNull()
{
// Note: validation should identify this issue first (values of correct
// type rule) however execution should still protect against this.
$this->assertQueryResult(
'query {
withNonNullArg(cannotBeNull: null)
}',
[
'data' => [
'withNonNullArg' => null,
],
'errors' => [
[
'message' => 'Argument "cannotBeNull" of non-null type "String!" must not be null.',
'locations' => [
[
'line' => 2,
'column' => 44,
],
],
'path' => ['withNonNullArg'],
],
],
]
);
}
public function testFieldErrorWhenNonNullArgNotProvidedVariableValue()
{
// Note: validation should identify this issue first (variables in allowed
// position rule) however execution should still protect against this.
$this->assertQueryResult(
'query ($testVar: String) {
withNonNullArg(cannotBeNull: $testVar)
}',
[
'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' => 2,
'column' => 44,
],
],
'path' => ['withNonNullArg'],
],
],
],
// Intentionally missing variable
[]
);
}
public function testFieldErrorWhenNonNullArgProvidedVariableWithExplicitNullValue()
{
$this->assertQueryResult(
'query ($testVar: String = "default value") {
withNonNullArg (cannotBeNull: $testVar)
}',
[
'data' => [
'withNonNullArg' => null,
],
'errors' => [
[
'message' => 'Argument "cannotBeNull" of non-null type "String!" must not be null.',
'locations' => [
[
'line' => 2,
'column' => 45,
],
],
'path' => ['withNonNullArg'],
],
],
],
['testVar' => null]
);
}
/**
* @param string $query
* @param array $expected
* @param array $variables
*/
private function assertQueryResult(string $query, array $expected, array $variables = []): void
{
$this->assertQueryResultWithSchema($this->schemaWithNonNullArg, $query, $expected, null, null, $variables);
}
}
================================================
FILE: tests/Functional/Execution/ResolveTest.php
================================================
newObjectType([
'name' => 'Query',
'fields' => [
'test' => $testField
]
])
]);
}
// Execute: resolve function
/**
* Default function accesses properties
*
* @throws \Digia\GraphQL\Error\InvariantException
*/
public function testDefaultFunctionAccessProperties()
{
$schema = $this->createTestSchema(['type' => stringType()]);
$source = ['test' => 'testValue'];
$result = graphql($schema, '{ test }', $source);
$this->assertEquals([
'data' => [
'test' => 'testValue'
]
], $result);
}
/**
* Default function calls methods
*
* @throws \Digia\GraphQL\Error\InvariantException
*/
public function testDefaultFunctionCallsMethods()
{
$schema = $this->createTestSchema(['type' => stringType()]);
$source = [
'test' => function () {
return 'secretValue';
}
];
$result = graphql($schema, '{ test }', $source);
$this->assertEquals([
'data' => [
'test' => 'secretValue'
]
], $result);
}
/**
* Default function passes args and context
*
* @throws \Digia\GraphQL\Error\InvariantException
*/
public function testDefaultFunctionPassesArgsAndContext()
{
$schema = $this->createTestSchema([
'type' => intType(),
'args' => ['addend1' => ['type' => intType()]],
]);
$source = new class(700)
{
private $num;
public function __construct($num)
{
$this->num = $num;
}
public function test($source, $args, $context)
{
return $this->num + $args['addend1'] + $context['addend2'];
}
};
$result = graphql($schema, '{ test(addend1: 80) }', $source, ['addend2' => 9]);
$this->assertEquals([
'data' => [
'test' => 789
]
], $result);
}
/**
* Uses provided resolve function
*
* @throws \Digia\GraphQL\Error\InvariantException
*/
public function testUsesProvidedResolveFunction()
{
$schema = $this->createTestSchema([
'type' => stringType(),
'args' => [
'aStr' => ['type' => stringType()],
'aInt' => ['type' => intType()],
],
'resolve' => function ($source, $args) {
return json_encode([$source, $args]);
}
]);
$result = graphql($schema, '{ test }');
$this->assertEquals([
'data' => [
'test' => '[null,[]]'
]
], $result);
$this->assertEquals([
'data' => [
'test' => '["Source!",[]]'
]
], graphql($schema, '{ test }', 'Source!'));
$this->assertEquals([
'data' => [
'test' => '["Source!",{"aStr":"String!"}]'
]
], graphql($schema, '{ test(aStr:"String!") }', 'Source!'));
$this->assertEquals([
'data' => [
'test' => '["Source!",{"aStr":"String!","aInt":-123}]'
]
], graphql($schema, '{ test(aInt: -123, aStr: "String!") }', 'Source!'));
}
}
================================================
FILE: tests/Functional/Execution/SchemaTest.php
================================================
'Image',
'fields' => [
'url' => ['type' => stringType()],
'height' => ['type' => intType()],
'width' => ['type' => intType()],
]
]);
$BlogAuthor = newObjectType([
'name' => 'Author',
'fields' => function () use (&$BlogArticle, &$BlogImage) {
return [
'id' => ['type' => stringType()],
'name' => ['type' => stringType()],
'pic' => [
'type' => $BlogImage,
'args' => [
'width' => ['type' => intType()],
'height' => ['type' => intType()]
],
'resolve' => function ($root, $args) {
['width' => $width, 'height' => $height] = $args;
return $root['pic']($width, $height);
}
],
'recentArticle' => ['type' => $BlogArticle],
];
}
]);
$BlogArticle = newObjectType([
'name' => 'Article',
'fields' => [
'id' => ['type' => newNonNull(stringType())],
'isPublished' => ['type' => booleanType()],
'author' => ['type' => $BlogAuthor],
'title' => ['type' => stringType()],
'body' => ['type' => stringType()],
'keywords' => ['type' => newList(stringType())],
]
]);
$Query = newObjectType([
'name' => 'Query',
'fields' => [
'article' => [
'type' => $BlogArticle,
'args' => [
'id' => ['type' => idType()]
],
'resolve' => function ($root, $args) use (&$article) {
return $article($args['id']);
}
],
'feed' => [
'type' => newList($BlogArticle),
'resolve' => function ($root, $args) use (&$article) {
return [
$article(1),
$article(2),
$article(3),
$article(4),
$article(5),
$article(6),
$article(7),
$article(8),
$article(9),
$article(10),
];
}
]
]
]);
$article = function ($id) use (&$johnSmith) {
return [
'id' => $id,
'isPublished' => 'true',
'author' => $johnSmith,
'title' => 'My Article ' . $id,
'body' => 'This is a post',
'hidden' => 'This data is not exposed in the schema',
'keywords' => ['foo', 'bar', 1, true, null],
];
};
$getPic = function ($uid, $width, $height) {
return [
'url' => "cdn://${uid}",
'width' => $width,
'height' => $height,
];
};
$johnSmith = function () use (&$article, &$getPic) {
return [
'id' => 123,
'name' => 'John Smith',
'pic' => function ($width, $height) use (&$getPic) {
return $getPic(123, $width, $height);
},
'recentArticle' => $article(1)
];
};
$BlogSchema = newSchema([
'query' => $Query
]);
$request = '
{
feed {
id,
title
},
article(id: "1") {
...articleFields,
author {
id,
name,
pic(width: 640, height: 480) {
url,
width,
height
},
recentArticle {
...articleFields,
keywords
}
}
}
}
fragment articleFields on Article {
id,
isPublished,
title,
body,
hidden,
notdefined
}
';
$result = execute($BlogSchema, parse($request));
$this->assertEquals([
'data' => [
'feed' => [
[
'id' => '1',
'title' => 'My Article 1',
],
[
'id' => '2',
'title' => 'My Article 2',
],
[
'id' => '3',
'title' => 'My Article 3',
],
[
'id' => '4',
'title' => 'My Article 4',
],
[
'id' => '5',
'title' => 'My Article 5',
],
[
'id' => '6',
'title' => 'My Article 6',
],
[
'id' => '7',
'title' => 'My Article 7',
],
[
'id' => '8',
'title' => 'My Article 8',
],
[
'id' => '9',
'title' => 'My Article 9',
],
[
'id' => '10',
'title' => 'My Article 10',
],
],
'article' => [
'id' => '1',
'isPublished' => true,
'title' => 'My Article 1',
'body' => 'This is a post',
'author' => [
'id' => '123',
'name' => 'John Smith',
'pic' => [
'url' => 'cdn://123',
'width' => 640,
'height' => 480,
],
'recentArticle' => [
'id' => '1',
'isPublished' => true,
'title' => 'My Article 1',
'body' => 'This is a post',
'keywords' => ['foo', 'bar', '1', 'true', null],
],
]
]
]
], $result->toArray());
}
public function testExecuteUsingASchemaWithCustomScalarType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$dateType = newScalarType([
'name' => 'Date',
'serialize' => function (\DateTime $value) {
/** @noinspection PhpUndefinedMethodInspection */
return $value->format('d.m.Y');
},
'parseValue' => function ($value) {
return new \DateTime($value);
},
'parseLiteral' => function ($node) {
/** @noinspection PhpUndefinedMethodInspection */
return new \DateTime($node->getValue(), new \DateTimeZone('Europe/Helsinki'));
},
]);
/** @noinspection PhpUnhandledExceptionInspection */
$TestInputObject = newInputObjectType([
'name' => 'TestInputObject',
'fields' => [
'c' => ['type' => newNonNull(stringType())],
'd' => ['type' => $dateType]
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$blogArticle = newObjectType([
'name' => 'Article',
'fields' => [
'id' => ['type' => newNonNull(stringType())],
'title' => ['type' => stringType()],
'body' => ['type' => stringType()],
'createdAt' => ['type' => $dateType],
]
]);
$article = function ($id, $date) {
return [
'id' => $id,
'title' => 'My Article ' . $id,
'body' => 'This is a post',
'createdAt' => $date,
];
};
/** @noinspection PhpUnhandledExceptionInspection */
$TestType = newObjectType([
'name' => 'TestType',
'fields' => [
'articles' => [
'type' => newList($blogArticle),
'args' => [
'input' => ['type' => $TestInputObject]
],
'resolve' => function ($root, $args) use (&$article) {
return [
$article(1, new \DateTime('2018-04-30')),
$article(2, new \DateTime('2018-05-01'))
];
}
]
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => $TestType
]);
$query = '{
articles(input: {c:"foo",d: "2018-01-01"}) {
id
title
createdAt
}
}';
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema, parse($query));
$this->assertEquals([
'data' => [
'articles' => [
[
'id' => '1',
'title' => 'My Article 1',
'createdAt' => "30.04.2018"
],
[
'id' => '2',
'title' => 'My Article 2',
'createdAt' => "01.05.2018"
],
]
]
], $result->toArray());
}
}
================================================
FILE: tests/Functional/Execution/UnionInterfaceTest.php
================================================
'Named',
'fields' => [
'name' => ['type' => stringType()]
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$DogType = newObjectType([
'name' => 'Dog',
'interfaces' => [$NamedType],
'fields' => [
'name' => ['type' => stringType()],
'woofs' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return $obj instanceof Dog;
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$CatType = newObjectType([
'name' => 'Cat',
'interfaces' => [$NamedType],
'fields' => [
'name' => ['type' => stringType()],
'meows' => ['type' => booleanType()],
],
'isTypeOf' => function ($obj) {
return $obj instanceof Cat;
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$PetType = newUnionType([
'name' => 'Pet',
'types' => [$DogType, $CatType],
'resolveType' => function ($result, $context, $info) use ($DogType, $CatType) {
if ($result instanceof Dog) {
return $DogType;
}
if ($result instanceof Cat) {
return $CatType;
}
return null;
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$PersonType = newObjectType([
'name' => 'Person',
'interfaces' => [$NamedType],
'fields' => [
'name' => ['type' => stringType()],
'pets' => ['type' => newList($PetType)],
'friends' => ['type' => newList($NamedType)],
],
'isTypeOf' => function ($obj) {
return $obj instanceof Person;
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => $PersonType
]);
$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->schema = $schema;
}
//EXECUTE: UNION AND INTERSECTION TYPES
/**
* can introspect on union and intersection types
*/
public function testCanIntrospectOnUnionAndIntersectionTypes()
{
$source = '
{
Named: __type(name: "Named") {
kind
name
fields { name }
interfaces { name }
possibleTypes { name }
enumValues { name }
inputFields { name }
}
Pet: __type(name: "Pet") {
kind
name
fields { name }
interfaces { name }
possibleTypes { name }
enumValues { name }
inputFields { name }
}
}';
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($this->schema, parse($source));
$this->assertSame([
'Named' => [
'kind' => 'INTERFACE',
'name' => 'Named',
'fields' => [
['name' => 'name']
],
'interfaces' => null,
'possibleTypes' => [
['name' => 'Person'],
['name' => 'Dog'],
['name' => 'Cat']
],
'enumValues' => null,
'inputFields' => null
],
'Pet' => [
'kind' => 'UNION',
'name' => 'Pet',
'fields' => null,
'interfaces' => null,
'possibleTypes' => [
['name' => 'Dog'],
['name' => 'Cat']
],
'enumValues' => null,
'inputFields' => null
]
], $result->getData());
}
/**
* executes using union types
*/
public function testExecutesUsingUnionTypes()
{
$source = '
{
__typename
name
pets {
__typename
name
woofs
meows
}
}';
$expected = [
'__typename' => 'Person',
'name' => 'John',
'pets' => [
['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => false],
['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true],
]
];
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($this->schema, parse($source), $this->john);
$this->assertEquals($expected, $result->getData());
}
/**
* executes union types with inline fragments
*/
public function testExecutesUnionTypesWithInlineFragments()
{
$source = '
{
__typename
name
pets {
__typename
... on Dog {
name
woofs
}
... on Cat {
name
meows
}
}
}';
$expected = [
'__typename' => 'Person',
'name' => 'John',
'pets' => [
['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => false],
['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true],
]
];
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($this->schema, parse($source), $this->john);
$this->assertSame($expected, $result->getData());
}
/**
* executes using interface types
*/
public function testExecutesUsingInterfaceTypes()
{
$source = '
{
__typename
name
friends {
__typename
name
woofs
meows
}
}';
$expected = [
'__typename' => 'Person',
'name' => 'John',
'friends' => [
['__typename' => 'Person', 'name' => 'Liz'],
['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true],
]
];
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($this->schema, parse($source), $this->john);
$this->assertSame($expected, $result->getData());
}
/**
* executes union types with inline fragments
*/
public function testExecutesUnionTypesWithInlineFragmentsTwo()
{
$source = '
{
__typename
name
friends {
__typename
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
$expected = [
'__typename' => 'Person',
'name' => 'John',
'friends' => [
['__typename' => 'Person', 'name' => 'Liz'],
['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true],
]
];
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($this->schema, parse($source), $this->john);
$this->assertSame($expected, $result->getData());
}
/**
* allows fragment conditions to be abstract types
*/
public function testAllowsFragmentConditionsToBeAbstractTypes()
{
$source = '
{
__typename
name
pets { ...PetFields }
friends { ...FriendFields }
}
fragment PetFields on Pet {
__typename
... on Dog {
name
woofs
}
... on Cat {
name
meows
}
}
fragment FriendFields on Named {
__typename
name
... on Dog {
woofs
}
... on Cat {
meows
}
}';
$expected = [
'__typename' => 'Person',
'name' => 'John',
'pets' => [
['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => false],
['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true]
],
'friends' => [
['__typename' => 'Person', 'name' => 'Liz'],
['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true]
]
];
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($this->schema, parse($source), $this->john);
$this->assertSame($expected, $result->getData());
}
/**
* gets execution info in resolver
*/
public function testGetsExecutionInfoInResolver()
{
$encounteredContext = null;
$encounteredSchema = null;
$encounteredRootValue = null;
$PersonType2 = null;
/** @noinspection PhpUnhandledExceptionInspection */
$NamedType2 = newInterfaceType([
'name' => 'Named',
'fields' => [
'name' => ['type' => stringType()]
],
'resolveType' => function ($obj, $context, ResolveInfo $info) use (
&$encounteredContext,
&$encounteredSchema,
&$encounteredRootValue,
&$PersonType2
) {
$encounteredContext = $context;
$encounteredSchema = $info->getSchema();
$encounteredRootValue = $info->getRootValue();
return $PersonType2;
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$PersonType2 = newObjectType([
'name' => 'Person',
'interfaces' => [$NamedType2],
'fields' => [
'name' => ['type' => stringType()],
'friends' => ['type' => newList($NamedType2)],
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$schema2 = newSchema([
'query' => $PersonType2
]);
$john2 = new Person('John', [], [$this->liz]);
$context = ['authToken' => '123abc'];
/** @noinspection PhpUnhandledExceptionInspection */
$result = execute($schema2, parse('{ name, friends { name } }'), $john2, $context);
$this->assertSame(
['name' => 'John', 'friends' => [['name' => 'Liz']]],
$result->getData()
);
$this->assertSame($context, $encounteredContext);
$this->assertSame($schema2, $encounteredSchema);
$this->assertSame($john2, $encounteredRootValue);
}
}
================================================
FILE: tests/Functional/Execution/ValuesResolverTest.php
================================================
valuesResolver = new ValuesResolver();
}
/**
* @throws \Digia\GraphQL\Error\InvalidTypeException
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Execution\ExecutionException
* @throws \Digia\GraphQL\Language\SyntaxErrorException
*/
public function testCoerceArgumentValues()
{
$schema = newSchema([
'query' => newObjectType([
'name' => 'Greeting',
'fields' => [
'greeting' => [
'type' => stringType(),
'args' => [
'name' => [
'type' => stringType(),
],
],
],
],
]),
]);
$documentNode = parse('query Hello($name: String) { Greeting(name: $name) }');
$operation = $documentNode->getDefinitions()[0];
$node = $operation->getSelectionSet()->getSelections()[0];
$definition = $schema->getQueryType()->getFields()['greeting'];
$context = new ExecutionContext(
$schema, [], null, null, ['name' => 'Han Solo'], null, $operation, []
);
$args = $this->valuesResolver->coerceArgumentValues($definition, $node, $context->getVariableValues());
$this->assertSame(['name' => 'Han Solo'], $args);
}
/**
* @throws \Digia\GraphQL\Error\GraphQLException
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Language\SyntaxErrorException
* @throws \Digia\GraphQL\Util\ConversionException
*/
public function testCoerceVariableValues(): void
{
$schema = newSchema([
'query' => newObjectType([
'name' => 'nonNullBoolean',
'fields' => [
'greeting' => [
'type' => stringType(),
'args' => [
'shout' => [
'type' => newNonNull(booleanType()),
],
],
],
],
]),
]);
$documentNode = parse('
query ($shout: Boolean!) {
nonNullBoolean(shout: $shout)
}
');
/** @var OperationDefinitionNode $operation */
$operation = $documentNode->getDefinitions()[0];
$variableDefinitions = $operation->getVariableDefinitions();
// Try with true and false and null (null should give errors, the rest shouldn't)
$coercedValue = $this->valuesResolver->coerceVariableValues($schema, $variableDefinitions, ['shout' => true]);
$this->assertSame(['shout' => true], $coercedValue->getValue());
$this->assertFalse($coercedValue->hasErrors());
$coercedValue = $this->valuesResolver->coerceVariableValues($schema, $variableDefinitions, ['shout' => false]);
$this->assertSame(['shout' => false], $coercedValue->getValue());
$this->assertFalse($coercedValue->hasErrors());
$coercedValue = $this->valuesResolver->coerceVariableValues($schema, $variableDefinitions, ['shout' => null]);
$this->assertEquals([], $coercedValue->getValue());
$this->assertTrue($coercedValue->hasErrors());
}
/**
* @throws \Digia\GraphQL\Error\GraphQLException
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Language\SyntaxErrorException
* @throws \Digia\GraphQL\Util\ConversionException
*/
public function testCoerceValuesForInputObjectTypes(): void
{
// Test input object types
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'inputObjectField' => [
'type' => booleanType(),
'args' => [
'inputObject' => [
'type' => newInputObjectType([
'name' => 'InputObject',
'fields' => [
'a' => ['type' => stringType()],
'b' => ['type' => newNonNull(stringType())]
]
]
)
],
],
],
],
]),
]);
$documentNode = parse('
query ($inputObject: InputObject!) {
inputObjectField(inputObject: $inputObject)
}
');
/** @var OperationDefinitionNode $operation */
$operation = $documentNode->getDefinitions()[0];
$variableDefinitions = $operation->getVariableDefinitions();
// Test with a missing non-null string
$coercedValue = $this->valuesResolver->coerceVariableValues($schema, $variableDefinitions, [
'inputObject' => [
'a' => 'some string'
]
]);
$this->assertTrue($coercedValue->hasErrors());
$this->assertEquals('Variable "$inputObject" got invalid value {"a":"some string"}; Field value.b of required type String! was not provided.',
$coercedValue->getErrors()[0]->getMessage());
// Test again with all variables, no errors expected
$coercedValue = $this->valuesResolver->coerceVariableValues($schema, $variableDefinitions, [
'inputObject' => [
'a' => 'some string',
'b' => 'some other required string',
]
]);
$this->assertFalse($coercedValue->hasErrors());
// Test with non-nullable boolean input fields
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'inputObjectField' => [
'type' => booleanType(),
'args' => [
'inputObject' => [
'type' => newInputObjectType([
'name' => 'InputObject',
'fields' => [
'a' => ['type' => booleanType()],
'b' => ['type' => newNonNull(booleanType())]
]
]
)
],
],
],
],
]),
]);
$documentNode = parse('
query ($inputObject: InputObject!) {
inputObjectField(inputObject: $inputObject)
}
');
/** @var OperationDefinitionNode $operation */
$operation = $documentNode->getDefinitions()[0];
$variableDefinitions = $operation->getVariableDefinitions();
// Test with a missing non-null string
$coercedValue = $this->valuesResolver->coerceVariableValues($schema, $variableDefinitions, [
'inputObject' => [
'a' => true
]
]);
$this->assertTrue($coercedValue->hasErrors());
$this->assertEquals('Variable "$inputObject" got invalid value {"a":true}; Field value.b of required type Boolean! was not provided.',
$coercedValue->getErrors()[0]->getMessage());
// Test again with all fields present, all booleans true
$coercedValue = $this->valuesResolver->coerceVariableValues($schema, $variableDefinitions, [
'inputObject' => [
'a' => true,
'b' => true,
]
]);
$this->assertFalse($coercedValue->hasErrors());
// Test again with all fields present, all booleans false (this has been problematic before)
$coercedValue = $this->valuesResolver->coerceVariableValues($schema, $variableDefinitions, [
'inputObject' => [
'a' => false,
'b' => false,
]
]);
$this->assertFalse($coercedValue->hasErrors());
}
}
================================================
FILE: tests/Functional/Execution/VariablesTest.php
================================================
'ComplexScalar',
'serialize' => function ($value) {
if ($value === 'DeserializedValue') {
return 'SerializedValue';
}
return null;
},
'parseValue' => function ($value) {
if ($value === 'SerializedValue') {
return 'DeserializedValue';
}
return null;
},
'parseLiteral' => function (ValueAwareInterface $ast) {
if ($ast->getValue() === 'SerializedValue') {
return 'DeserializedValue';
}
return null;
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$TestInputObject = newInputObjectType([
'name' => 'TestInputObject',
'fields' => [
'a' => ['type' => stringType()],
'b' => ['type' => newList(stringType())],
'c' => ['type' => newNonNull(stringType())],
'd' => ['type' => $TestComplexScalar]
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$TestNestedInputObject = newInputObjectType([
'name' => 'TestNestedInputObject',
'fields' => [
'na' => ['type' => newNonNull($TestInputObject)],
'nb' => ['type' => newNonNull(stringType())]
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$TestType = newObjectType([
'name' => 'TestType',
'fields' => [
'fieldWithObjectInput' => $this->fieldWithInputArg(['type' => $TestInputObject]),
'fieldWithNullableStringInput' => $this->fieldWithInputArg(['type' => stringType()]),
'fieldWithNonNullableStringInput' => $this->fieldWithInputArg([
'type' => newNonNull(stringType())
]),
'fieldWithDefaultArgumentValue' => $this->fieldWithInputArg([
'type' => stringType(),
'defaultValue' => 'Hello World'
]),
'fieldWithNonNullableStringInputAndDefaultArgumentValue' => $this->fieldWithInputArg([
'type' => newNonNull(stringType()),
'defaultValue' => 'Hello World'
]),
'fieldWithNestedInputObject' => $this->fieldWithInputArg([
'type' => $TestNestedInputObject,
'defaultValue' => 'Hello World'
]),
'list' => $this->fieldWithInputArg([
'type' => newList(stringType())
]),
'nnList' => $this->fieldWithInputArg([
'type' => newNonNull(newList(stringType())),
]),
'listNN' => $this->fieldWithInputArg([
'type' => newList(newNonNull(stringType())),
]),
'nnListNN' => $this->fieldWithInputArg([
'type' => newNonNull(newList(newNonNull(stringType()))),
]),
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->schema = newSchema([
'query' => $TestType
]);
}
/**
* @param array $inputArg
* @return array
*/
function fieldWithInputArg(array $inputArg): array
{
return [
'type' => stringType(),
'args' => [
'input' => $inputArg
],
'resolve' => function ($_, $args) {
return array_key_exists('input', $args) ? jsonEncode($args['input']) : null;
}
];
}
// Execute: Handles inputs
// Handles objects and nullability
public function testExecutesWithComplexInput()
{
$this->assertQueryResult(
'{
fieldWithObjectInput(input: {a: "foo", b: ["bar"], c: "baz"})
}',
[
'data' => [
'fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}'
]
]
);
}
public function testVariableNotProvided()
{
$this->assertQueryResult(
'query q($input: String) {
fieldWithNullableStringInput(input: $input)
}',
[
'data' => [
'fieldWithNullableStringInput' => null
]
],
// Intentionally missing variable values.
[]
);
}
public function testVariableWithExplicitNullValue()
{
$this->assertQueryResult(
'query q($input: String) {
fieldWithNullableStringInput(input: $input)
}',
[
'data' => [
'fieldWithNullableStringInput' => 'null',
]
],
['input' => null]
);
}
public function testUsesDefaultValueWhenValueNotProvided()
{
$this->assertQueryResult(
'query ($input: TestInputObject = {a: "foo", b: ["bar"], c: "baz"}) {
fieldWithObjectInput(input: $input)
}',
[
'data' => [
'fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}',
]
]
);
}
public function testDoesNotUseDefaultValueWhenValueProvided()
{
$this->assertQueryResult(
'query ($input: String = "Default value") {
fieldWithNullableStringInput(input: $input)
}',
[
'data' => [
'fieldWithNullableStringInput' => '"Variable value"',
]
],
['input' => 'Variable value']
);
}
public function testUsesExplicitNullValueInsteadOfDefaultValue()
{
$this->assertQueryResult(
'query ($input: String = "Default value") {
fieldWithNullableStringInput(input: $input)
}',
[
'data' => [
'fieldWithNullableStringInput' => 'null',
]
],
['input' => null]
);
}
public function testUsesNullDefaultValueWhenValueNotProvided()
{
$this->assertQueryResult(
'query ($input: String = null) {
fieldWithNullableStringInput(input: $input)
}',
[
'data' => [
'fieldWithNullableStringInput' => 'null',
]
],
// Intentionally missing variable values.
[]
);
}
public function testProperlyParsesSingleValueToList()
{
$this->assertQueryResult(
'{
fieldWithObjectInput(input: {a: "foo", b: "bar", c: "baz"})
}',
[
'data' => [
'fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}'
]
]
);
}
public function testProperlyParsesNullValueToNull()
{
$this->assertQueryResult(
'{
fieldWithObjectInput(input: {a: null, b: null, c: "C", d: null})
}',
[
'data' => [
'fieldWithObjectInput' => '{"a":null,"b":null,"c":"C","d":null}'
]
]
);
}
public function testProperlyParsesNullValueInList()
{
$this->assertQueryResult(
'{
fieldWithObjectInput(input: {b: ["A",null,"C"], c: "C"})
}',
[
'data' => [
'fieldWithObjectInput' => '{"b":["A",null,"C"],"c":"C"}'
]
]
);
}
public function testDoesNotUseIncorrectValue()
{
$this->assertQueryResult(
'{
fieldWithObjectInput(input: ["foo", "bar", "baz"])
}',
[
'data' => [
'fieldWithObjectInput' => null
],
'errors' => [
[
'message' => 'Argument "input" has invalid value ["foo","bar","baz"].',
'path' => ['fieldWithObjectInput'],
'locations' => [['line' => 2, 'column' => 43]],
]
]
]
);
}
public function testProperlyRunsParseLiteralOnComplexScalarTypes()
{
$this->assertQueryResult(
'{
fieldWithObjectInput(input: {c: "foo", d: "SerializedValue"})
}',
[
'data' => [
'fieldWithObjectInput' => '{"c":"foo","d":"DeserializedValue"}'
]
]
);
}
// USING VARIABLES
public function testExecutesWithComplexInputUsingVariables()
{
$this->assertQueryResult(
'query ($input: TestInputObject) {
fieldWithObjectInput(input: $input)
}',
[
'data' => [
'fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}'
]
],
['input' => ['a' => 'foo', 'b' => ['bar'], 'c' => 'baz']]
);
}
public function testUseDefaultValueWhenNotProvide()
{
$this->assertQueryResult(
'query ($input: TestInputObject = {a: "foo", b: ["bar"], c: "baz"}) {
fieldWithObjectInput(input: $input)
}',
[
'data' => [
'fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}'
]
]
);
}
public function testProperlyParsesSingleValueToListUsingVariable()
{
$this->assertQueryResult(
'query ($input: TestInputObject) {
fieldWithObjectInput(input: $input)
}',
[
'data' => [
'fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}'
]
],
['input' => ['a' => 'foo', 'b' => 'bar', 'c' => 'baz']]
);
}
public function testExecutesWithComplexScalarInputUsingVariable()
{
$this->assertQueryResult(
'query ($input: TestInputObject) {
fieldWithObjectInput(input: $input)
}',
[
'data' => [
'fieldWithObjectInput' => '{"c":"foo","d":"DeserializedValue"}'
]
],
['input' => ['c' => 'foo', 'd' => 'SerializedValue']]
);
}
public function testErrorsOnNullForNestedNonNullUsingVariable()
{
$this->assertQueryResult(
'query ($input: TestInputObject) {
fieldWithObjectInput(input: $input)
}',
[
'data' => null,
'errors' => [
[
'message' => 'Variable "$input" got invalid value {"a":"foo","b":"bar","c":null}; ' .
'Field value.c of required type String! was not provided.',
'locations' => [
[
'line' => 1,
'column' => 8
]
],
]
]
],
['input' => ['a' => 'foo', 'b' => 'bar', 'c' => null]]
);
}
public function testErrorsOnDeepNestedErrorsWithManyErrors()
{
$this->assertQueryResult(
'query ($input: TestNestedInputObject) {
fieldWithNestedObjectInput(input: $input)
}',
[
'data' => null,
'errors' => [
[
'message' => 'Variable "$input" got invalid value {"na":{"a":"foo"}}; ' .
'Field value.na.c of required type String! was not provided.',
'locations' => [
[
'line' => 1,
'column' => 8
]
],
],
[
'message' => 'Variable "$input" got invalid value {"na":{"a":"foo"}}; ' .
'Field value.nb of required type String! was not provided.',
'locations' => [
[
'line' => 1,
'column' => 8
]
],
],
],
],
['input' => ['na' => ['a' => 'foo']]]
);
}
public function testErrorsOnAdditionOfUnknownInputField()
{
$this->assertQueryResult(
'query ($input: TestInputObject) {
fieldWithObjectInput(input: $input)
}',
[
'data' => null,
'errors' => [
[
'message' => 'Variable "$input" got invalid value ' .
'{"a":"foo","b":"bar","c":"baz","extra":"dog"}; ' .
'Field "extra" is not defined by type TestInputObject.',
'locations' => [
[
'line' => 1,
'column' => 8
]
],
]
]
],
['input' => ['a' => 'foo', 'b' => 'bar', 'c' => 'baz', 'extra' => 'dog']]
);
}
// HANDLES NULLABLE SCALARS
public function testAllowsNullableInputsToBeOmitted()
{
$this->assertQueryResult(
'{
fieldWithNullableStringInput
}',
[
'data' => [
'fieldWithNullableStringInput' => null
],
]
);
}
public function testAllowsNullableInputsToBeOmittedInAVariable()
{
$this->assertQueryResult(
'query ($value: String) {
fieldWithNullableStringInput(input: $value)
}',
[
'data' => [
'fieldWithNullableStringInput' => null
],
]
);
}
public function testAllowsNullableInputsToBeOmittedInUnlistedVariable()
{
$this->assertQueryResult(
'query {
fieldWithNullableStringInput(input: $value)
}',
[
'data' => [
'fieldWithNullableStringInput' => null
],
]
);
}
public function testAllowsNullableInputsToBeSetToNullInVariable()
{
$this->assertQueryResult(
'query ($value: String) {
fieldWithNullableStringInput(input: $value)
}',
[
'data' => [
'fieldWithNullableStringInput' => 'null'
],
],
['value' => null]
);
}
public function testAllowsNullableInputsToBeSetToAValueDirectly()
{
$this->assertQueryResult(
'query ($value: String) {
fieldWithNullableStringInput(input: "a")
}',
[
'data' => [
'fieldWithNullableStringInput' => '"a"'
],
]
);
}
// HANDLES NON-NULLABLE SCALARS'
public function testAllowsNonNullableInputsToBeOmittedGivenADefault()
{
$this->assertQueryResult(
'query ($value: String = "default") {
fieldWithNonNullableStringInput(input: $value)
}',
[
'data' => [
'fieldWithNonNullableStringInput' => '"default"'
],
]
);
}
public function testDoesNotAllowNonNullableInputsToBeOmittedInAVariable()
{
$this->assertQueryResult(
'query ($value: String!) {
fieldWithNonNullableStringInput(input: $value)
}',
[
'data' => null,
'errors' => [
[
'message' => 'Variable "$value" of required type "String!" was not provided.',
'locations' => [
[
'column' => 8,
'line' => 1
]
],
],
],
]
);
}
public function testDoesNotAllowNonNullabeInputsToBeSetToNullInAVariable()
{
$this->assertQueryResult(
'query ($value: String!) {
fieldWithNonNullableStringInput(input: $value)
}',
[
'data' => null,
'errors' => [
[
'message' => 'Variable "$value" of required type "String!" was not provided.',
'locations' => [
[
'column' => 8,
'line' => 1
]
],
],
],
],
['value' => null]
);
}
public function testAllowsNonNullableInputsToBeSetToAValueInAVariable()
{
$this->assertQueryResult(
'query ($value: String!) {
fieldWithNonNullableStringInput(input: $value)
}',
[
'data' => [
'fieldWithNonNullableStringInput' => '"a"'
]
],
['value' => 'a']
);
}
public function testAllowsNonNullableInputsToBeSetToAValueDirectly()
{
$this->assertQueryResult(
'{
fieldWithNonNullableStringInput(input: "a")
}',
[
'data' => [
'fieldWithNonNullableStringInput' => '"a"'
]
]
);
}
public function testReportErrorForMissingNonNullableInputs()
{
$this->assertQueryResult(
'{ fieldWithNonNullableStringInput }',
[
'data' => [
'fieldWithNonNullableStringInput' => null
],
'errors' => [
[
'message' => 'Argument "input" of required type "String!" was not provided.',
'locations' => [
[
'line' => 1,
'column' => 3
]
],
'path' => ['fieldWithNonNullableStringInput']
]
]
]
);
}
public function testReportErrorForArrayPassedIntoStringInput()
{
$this->assertQueryResult(
'query ($value: String!) {
fieldWithNonNullableStringInput(input: $value)
}',
[
'data' => null,
'errors' => [
[
'message' => 'Variable "$value" got invalid value [1,2,3]; Expected type String; ' .
'String cannot represent a non-scalar value',
'locations' => [
[
'line' => 1,
'column' => 8
]
],
]
]
],
['value' => [1, 2, 3]]
);
}
public function testSerializingAnArrayViaGraphQLStringThrowsTypeError()
{
$this->expectException(InvalidTypeException::class);
stringType()->serialize([1, 2, 3]);
}
public function testReportErrorForNonProvidedVariableForNonNullableInputs()
{
// Note: this test would typically fail validation before encountering
// this execution error, however for queries which previously validated
// and are being run against a new schema which have introduced a breaking
// change to make a formerly non-required argument required, this asserts
// failure before allowing the underlying code to receive a non-null value.
$this->assertQueryResult(
'{
fieldWithNonNullableStringInput(input: $foo)
}',
[
'data' => [
'fieldWithNonNullableStringInput' => null
],
'errors' => [
[
'message' => 'Argument "input" of required type "String!" was provided the ' .
'variable "$foo" which was not provided a runtime value.',
'locations' => [
[
'line' => 2,
'column' => 54
]
],
'path' => ['fieldWithNonNullableStringInput']
]
]
]
);
}
// HANDLES LISTS AND NULLABILITY
public function testAllowsListsToBeNull()
{
$this->assertQueryResult(
'query ($input: [String]) {
list(input: $input)
}',
[
'data' => [
'list' => 'null'
],
],
['input' => null]
);
}
public function testAllowsListsContainValues()
{
$this->assertQueryResult(
'query ($input: [String]) {
list(input: $input)
}',
[
'data' => [
'list' => '["A"]'
],
],
['input' => ['A']]
);
}
public function testAllowsListsContainNull()
{
$this->assertQueryResult(
'query ($input: [String]) {
list(input: $input)
}',
[
'data' => [
'list' => '["A",null,"B"]'
],
],
['input' => ['A', null, 'B']]
);
}
public function testDoesNotAllowNonNullListsToBeNull()
{
$this->assertQueryResult(
'query ($input: [String]!) {
nnList(input: $input)
}',
[
'data' => null,
'errors' => [
[
'message' => 'Variable "$input" of required type "[String]!" was not provided.',
'locations' => [
[
'line' => 1,
'column' => 8
]
],
],
],
],
['input' => null]
);
}
public function testAllowsNonNullListsToContainValues()
{
$this->assertQueryResult(
'query ($input: [String]!) {
nnList(input: $input)
}',
[
'data' => [
'nnList' => '["A"]'
]
],
['input' => ['A']]
);
}
public function testAllowsListsOfNonNullsToBeNull()
{
$this->assertQueryResult(
'query ($input: [String!]) {
listNN(input: $input)
}',
[
'data' => [
'listNN' => 'null'
]
],
['input' => null]
);
}
public function testAllowsListsOfNonNullsToContainValues()
{
$this->assertQueryResult(
'query ($input: [String!]) {
listNN(input: $input)
}',
[
'data' => [
'listNN' => '["A"]'
]
],
['input' => ['A']]
);
}
public function testDoesNotAllowsListsOfNonNullsToContainNull()
{
$this->assertQueryResult(
'query ($input: [String!]) {
listNN(input: $input)
}',
[
'data' => null,
'errors' => [
[
'message' => 'Variable "$input" got invalid value ["A",null,"B"]; ' .
'Expected non-nullable type String! not to be null at value[1].',
'locations' => [
[
'line' => 1,
'column' => 8
]
],
],
],
],
['input' => ['A', null, 'B']]
);
}
public function testDoesNotAllowsListsOfNonNullsToBeNull()
{
$this->assertQueryResult(
'query ($input: [String!]!) {
nnListNN(input: $input)
}',
[
'data' => null,
'errors' => [
[
'message' => 'Variable "$input" of required type "[String!]!" was not provided.',
'locations' => [
[
'line' => 1,
'column' => 8
]
],
],
],
],
['input' => null]
);
}
public function testDoesNotAllowsNonNullListsOfNonNullsToContainNull()
{
$this->assertQueryResult(
'query ($input: [String!]!) {
nnListNN(input: $input)
}',
[
'data' => null,
'errors' => [
[
'message' => 'Variable "$input" got invalid value ["A",null,"B"]; ' .
'Expected non-nullable type String! not to be null at value[1].',
'locations' => [
[
'line' => 1,
'column' => 8
]
],
],
],
],
['input' => ['A', null, 'B']]
);
}
public function testDoesNotAllowsInvalidTypesToBeUsedAsValues()
{
$this->assertQueryResult(
'query ($input: TestType!) {
fieldWithObjectInput(input: $input)
}',
[
'data' => null,
'errors' => [
[
'message' => 'Variable "$input" expected value of type "TestType!" which ' .
'cannot be used as an input type.',
'locations' => [
[
'line' => 1,
'column' => 8
]
],
]
],
],
['input' => ['list' => ['A', null, 'B']]]
);
}
public function testDoesNotAllowsUnknownTypesToBeUsedAsValues()
{
$this->assertQueryResult(
'query ($input: UnknownType!) {
fieldWithObjectInput(input: $input)
}',
[
'data' => null,
'errors' => [
[
'message' => 'Variable "$input" expected value of type "UnknownType!" which ' .
'cannot be used as an input type.',
'locations' => [
[
'line' => 1,
'column' => 8
]
],
]
],
],
['input' => 'whoknows']
);
}
// Execute: Uses argument default values
public function testWhenNoArgumentProvided()
{
$this->assertQueryResult(
'{ fieldWithDefaultArgumentValue }',
[
'data' => ['fieldWithDefaultArgumentValue' => '"Hello World"']
]
);
}
public function testWhenOmittedVariableProvided()
{
$this->assertQueryResult(
'query ($optional: String) {
fieldWithDefaultArgumentValue(input: $optional)
}',
[
'data' => ['fieldWithDefaultArgumentValue' => '"Hello World"']
]
);
}
public function testNotWhenArgumentCannotBeCoerced()
{
$this->assertQueryResult(
'{
fieldWithDefaultArgumentValue(input: WRONG_TYPE)
}',
[
'data' => [
'fieldWithDefaultArgumentValue' => null
],
'errors' => [
[
'message' => 'Argument "input" has invalid value WRONG_TYPE.',
'locations' => [
[
'line' => 2,
'column' => 52
]
],
'path' => ['fieldWithDefaultArgumentValue']
]
],
]
);
}
public function testCustomDateTimeScalarType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$dateType = newScalarType([
'name' => 'Date',
'serialize' => function (\DateTime $value) {
/** @noinspection PhpUndefinedMethodInspection */
return $value->format('Y-m-d');
},
'parseValue' => function ($node) {
/** @noinspection PhpUndefinedMethodInspection */
return new \DateTime($node->getValue(), new \DateTimeZone('Europe/Helsinki'));
},
'parseLiteral' => function ($node) {
/** @noinspection PhpUndefinedMethodInspection */
return new \DateTime($node->getValue(), new \DateTimeZone('Europe/Helsinki'));
},
]);
/** @noinspection PhpUnhandledExceptionInspection */
$TestInputObject = newInputObjectType([
'name' => 'TestInputObject',
'fields' => [
'a' => ['type' => stringType()],
'b' => ['type' => newList(stringType())],
'c' => ['type' => newNonNull(stringType())],
'd' => ['type' => $dateType]
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$TestType = newObjectType([
'name' => 'TestType',
'fields' => [
'fieldWithObjectInput' => $this->fieldWithInputArg(['type' => $TestInputObject]),
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => $TestType
]);
$this->assertQueryResultWithSchema(
$schema,
'{
fieldWithObjectInput(input: {c: "foo", d: "2018-01-01"})
}',
[
'data' => [
'fieldWithObjectInput' => '{"c":"foo","d":{"date":"2018-01-01 00:00:00.000000","timezone_type":3,"timezone":"Europe\/Helsinki"}}'
]
]
);
}
/**
* @noinspection PhpDocMissingThrowsInspection
*
* @param string $query
* @param array $expected
* @param array $variables
*/
protected function assertQueryResult(string $query, array $expected, array $variables = [])
{
$this->assertQueryResultWithSchema($this->schema, $query, $expected, null, null, $variables);
}
}
================================================
FILE: tests/Functional/Execution/testClasses.php
================================================
name = $name;
}
}
class Person
{
public $name;
public $pets;
public $friends;
public function __construct(string $name, array $pets = [], array $friends = [])
{
$this->name = $name;
$this->friends = $friends;
$this->pets = $pets;
}
}
class Dog
{
public $name;
public $woofs;
public function __construct(string $name, bool $woofs)
{
$this->name = $name;
$this->woofs = $woofs;
}
}
class Cat
{
public $name;
public $meows;
public function __construct(string $name, bool $meows)
{
$this->name = $name;
$this->meows = $meows;
}
}
================================================
FILE: tests/Functional/IntrospectionTest.php
================================================
assertEquals([
'data' => [
'__schema' => [
'types' => [
['name' => 'Query'],
['name' => 'Episode'],
['name' => 'Character'],
['name' => 'String'],
['name' => 'Human'],
['name' => 'Droid'],
['name' => '__Schema'],
['name' => '__Type'],
['name' => '__TypeKind'],
['name' => 'Boolean'],
['name' => '__Field'],
['name' => '__InputValue'],
['name' => '__EnumValue'],
['name' => '__Directive'],
['name' => '__DirectiveLocation'],
],
],
],
], $result);
}
// Allows querying the schema for query type
public function testAllowsQueryingTheSchemaForQueryType()
{
$query = '
query IntrospectionQueryTypeQuery {
__schema {
queryType {
name
}
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'__schema' => [
'queryType' => [
'name' => 'Query',
],
],
],
], $result);
}
// Allows querying the schema for a specific type
public function testAllowsQueryingTheSchemaForASpecificType()
{
$query = '
query IntrospectionDroidTypeQuery {
__type(name: "Droid") {
name
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'__type' => [
'name' => 'Droid',
],
],
], $result);
}
// Allows querying the schema for an object kind
public function testAllowsQueryingTheSchemaForAnObjectKind()
{
$query = '
query IntrospectionDroidKindQuery {
__type(name: "Droid") {
name
kind
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'__type' => [
'name' => 'Droid',
'kind' => 'OBJECT',
],
],
], $result);
}
// Allows querying the schema for an interface kind
public function testAllowsQueryingTheSchemaForAnInterfaceKind()
{
$query = '
query IntrospectionCharacterKindQuery {
__type(name: "Character") {
name
kind
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'__type' => [
'name' => 'Character',
'kind' => 'INTERFACE',
],
],
], $result);
}
// Allows querying the schema for object fields
public function testAllowsQueryingTheSchemaForObjectFields()
{
$query = '
query IntrospectionDroidFieldsQuery {
__type(name: "Droid") {
name
fields {
name
type {
name
kind
}
}
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'__type' => [
'name' => 'Droid',
'fields' => [
[
'name' => 'id',
'type' => [
'name' => null,
'kind' => 'NON_NULL',
],
],
[
'name' => 'name',
'type' => [
'name' => 'String',
'kind' => 'SCALAR',
],
],
[
'name' => 'friends',
'type' => [
'name' => null,
'kind' => 'LIST',
],
],
[
'name' => 'appearsIn',
'type' => [
'name' => null,
'kind' => 'LIST',
],
],
[
'name' => 'secretBackstory',
'type' => [
'name' => 'String',
'kind' => 'SCALAR',
],
],
[
'name' => 'primaryFunction',
'type' => [
'name' => 'String',
'kind' => 'SCALAR',
],
],
],
],
],
], $result);
}
// Allows querying the schema for nested object fields
public function testAllowsQueryingTheSchemaForNestedObjectFields()
{
$query = '
query IntrospectionDroidNestedFieldsQuery {
__type(name: "Droid") {
name
fields {
name
type {
name
kind
ofType {
name
kind
}
}
}
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'__type' => [
'name' => 'Droid',
'fields' => [
[
'name' => 'id',
'type' => [
'name' => null,
'kind' => 'NON_NULL',
'ofType' => [
'name' => 'String',
'kind' => 'SCALAR',
],
],
],
[
'name' => 'name',
'type' => [
'name' => 'String',
'kind' => 'SCALAR',
'ofType' => null,
],
],
[
'name' => 'friends',
'type' => [
'name' => null,
'kind' => 'LIST',
'ofType' => [
'name' => 'Character',
'kind' => 'INTERFACE',
],
],
],
[
'name' => 'appearsIn',
'type' => [
'name' => null,
'kind' => 'LIST',
'ofType' => [
'name' => 'Episode',
'kind' => 'ENUM',
],
],
],
[
'name' => 'secretBackstory',
'type' => [
'name' => 'String',
'kind' => 'SCALAR',
'ofType' => null,
],
],
[
'name' => 'primaryFunction',
'type' => [
'name' => 'String',
'kind' => 'SCALAR',
'ofType' => null,
],
],
],
],
],
], $result);
}
// Allows querying the schema for field args
public function testAllowsQueryingTheSchemaForFieldArguments()
{
$query = '
query IntrospectionQueryTypeQuery {
__schema {
queryType {
fields {
name
args {
name
description
type {
name
kind
ofType {
name
kind
}
}
defaultValue
}
}
}
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'__schema' => [
'queryType' => [
'fields' => [
[
'name' => 'hero',
'args' => [
[
'defaultValue' => null,
'description' =>
'If omitted, returns the hero of the whole ' .
'saga. If provided, returns the hero of ' .
'that particular episode.',
'name' => 'episode',
'type' => [
'kind' => 'ENUM',
'name' => 'Episode',
'ofType' => null,
],
],
],
],
[
'name' => 'human',
'args' => [
[
'name' => 'id',
'description' => 'id of the human',
'type' => [
'kind' => 'NON_NULL',
'name' => null,
'ofType' => [
'kind' => 'SCALAR',
'name' => 'String',
],
],
'defaultValue' => null,
],
],
],
[
'name' => 'droid',
'args' => [
[
'name' => 'id',
'description' => 'id of the droid',
'type' => [
'kind' => 'NON_NULL',
'name' => null,
'ofType' => [
'kind' => 'SCALAR',
'name' => 'String',
],
],
'defaultValue' => null,
],
],
],
],
],
],
],
], $result);
}
// Allows querying the schema for documentation
public function testAllowsQueryingTheSchemaForDocumentation()
{
$query = '
query IntrospectionDroidDescriptionQuery {
__type(name: "Droid") {
name
description
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'__type' => [
'name' => 'Droid',
'description' => 'A mechanical creature in the Star Wars universe.',
],
],
], $result);
}
/**
* Test to check that we can introspect on a scalar, which does *not*
* have a name which clashes with a global PHP function or class
*/
public function testCanIntrospectOnANonClashingScalar()
{
$schema = '
scalar Postcode
type Query {
hello: Postcode
}
';
$schema = buildSchema($schema);
$query = '
query IntrospectionDroidDescriptionQuery {
__type(name: "Postcode") {
name
}
}
';
$result = graphql($schema, $query);
$this->assertArrayNotHasKey('errors', $result);
$this->assertEquals([
'data' => [
'__type' => [
'name' => 'Postcode',
],
],
], $result);
}
/**
* Test to check that we can introspect on a scalar, which *does*
* have a name which clashes with a global PHP function or class
*/
public function testCanIntrospectOnScalarWithClashingName()
{
$schema = '
scalar Date
type Query {
hello: Date
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema($schema);
$query = '
query IntrospectionDroidDescriptionQuery {
__type(name: "Date") {
name
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql($schema, $query);
$this->assertArrayNotHasKey('errors', $result);
$this->assertEquals([
'data' => [
'__type' => [
'name' => 'Date',
],
],
], $result);
}
}
================================================
FILE: tests/Functional/Language/FileSourceBuilderTest.php
================================================
build();
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\FileNotFoundException
*/
public function testSuccess(): void
{
$builder = new FileSourceBuilder(__DIR__ . '/schema-kitchen-sink.graphqls');
$source = $builder->build();
$this->assertGreaterThan(0, $source->getBodyLength());
}
}
================================================
FILE: tests/Functional/Language/MultiFileSourceBuilderTest.php
================================================
build();
$this->assertEquals(3747, $source->getBodyLength());
}
}
================================================
FILE: tests/Functional/Language/ParserTest.php
================================================
parseName(new Source('foo'));
$this->assertInstanceOf(NameNode::class, $nameNode);
$this->assertEquals('foo', $nameNode->getValue());
/** @var OperationDefinitionNode $operationDefinition */
$operationDefinition = $parser->parseOperationDefinition(new Source('query FooQuery { foo }'));
$this->assertInstanceOf(OperationDefinitionNode::class, $operationDefinition);
$this->assertEquals('query', $operationDefinition->getOperation());
$this->assertEquals('FooQuery', $operationDefinition->getNameValue());
/** @var VariableDefinitionNode $variableDefinitionNode */
$variableDefinitionNode = $parser->parseVariableDefinition(new Source('$foo: String = "bar"'));
$this->assertInstanceOf(VariableDefinitionNode::class, $variableDefinitionNode);
$this->assertEquals('foo', $variableDefinitionNode->getVariable()->getNameValue());
/** @noinspection PhpUndefinedMethodInspection */
$this->assertEquals('String', $variableDefinitionNode->getType()->getNameValue());
/** @noinspection PhpUndefinedMethodInspection */
$this->assertEquals('bar', $variableDefinitionNode->getDefaultValue()->getValue());
/** @var VariableNode $variableNode */
$variableNode = $parser->parseVariable(new Source('$foo'));
$this->assertInstanceOf(VariableNode::class, $variableNode);
$this->assertEquals('foo', $variableNode->getNameValue());
/** @var ArgumentNode $argumentNode */
$argumentNode = $parser->parseArgument(new Source('foo: String'));
$this->assertInstanceOf(ArgumentNode::class, $argumentNode);
$this->assertEquals('foo', $argumentNode->getNameValue());
/** @var DirectiveNode $directiveNode */
$directiveNode = $parser->parseDirective(new Source('@foo(bar: String, baz: Int)'));
$directiveArgs = $directiveNode->getArguments();
$this->assertInstanceOf(DirectiveNode::class, $directiveNode);
$this->assertEquals('foo', $directiveNode->getNameValue());
$this->assertEquals('bar', $directiveArgs[0]->getNameValue());
$this->assertEquals('baz', $directiveArgs[1]->getNameValue());
}
public function testParseProvidesUsefulErrors()
{
$this->expectException(SyntaxErrorException::class);
$this->expectExceptionMessage('Expected Name, found ');
/** @noinspection PhpUnhandledExceptionInspection */
parse('{');
$this->expectException(SyntaxErrorException::class);
$this->expectExceptionMessage('Expected {, found ');
/** @noinspection PhpUnhandledExceptionInspection */
parse('query', 'MyQuery.graphql');
}
public function testParsesVariableInlineValues()
{
/** @noinspection PhpUnhandledExceptionInspection */
parse('{ field(complex: { a: { b: [ $var ] } }) }');
$this->addToAssertionCount(1);
}
public function testParsesConstantDefaultValues()
{
$this->expectException(SyntaxErrorException::class);
$this->expectExceptionMessage('Unexpected $');
/** @noinspection PhpUnhandledExceptionInspection */
parse('query Foo($x: Complex = { a: { b: [ $var ] } }) { field }');
$this->addToAssertionCount(1);
}
public function testDoesNotAcceptFragmentsNamedOn()
{
$this->expectException(SyntaxErrorException::class);
$this->expectExceptionMessage('Unexpected Name "on"');
/** @noinspection PhpUnhandledExceptionInspection */
parse('fragment on on on { on }');
$this->addToAssertionCount(1);
}
public function testDoesNotAcceptFragmentSpreadOfOn()
{
$this->expectException(SyntaxErrorException::class);
$this->expectExceptionMessage('Expected Name, found }');
/** @noinspection PhpUnhandledExceptionInspection */
parse('{ ...on }');
$this->addToAssertionCount(1);
}
public function testParsesMultiByteCharacters()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse(dedent('
# This comment has a \u0A0A multi-byte character.
{ field(arg: "Has a \u0A0A multi-byte character.") }
'));
$this->assertArraySubset([
'definitions' => [
[
'selectionSet' => [
'selections' => [
[
'arguments' => [
[
'value' => [
'kind' => NodeKindEnum::STRING,
'value' => 'Has a \u0A0A multi-byte character.',
],
],
],
],
],
],
],
],
], $node->toAST());
}
public function testParsesKitchenSink()
{
$kitchenSink = readFileContents(__DIR__ . '/kitchen-sink.graphql');
/** @noinspection PhpUnhandledExceptionInspection */
parse($kitchenSink);
$this->addToAssertionCount(1);
}
public function testAllowsNonKeywordsAnywhereANameIsAllowed()
{
$nonKeywords = [
'on',
'fragment',
'query',
'mutation',
'subscription',
'true',
'false',
];
foreach ($nonKeywords as $keyword) {
$fragmentName = $keyword;
// You can't define or reference a fragment named `on`.
if ($keyword === 'on') {
$fragmentName = 'a';
}
/** @noinspection PhpUnhandledExceptionInspection */
parse(dedent("
query $keyword {
... $fragmentName
... on $keyword { field }
}
fragment $fragmentName on Type {
$keyword($keyword: $$keyword)
@$keyword($keyword: $keyword)
}
"));
$this->addToAssertionCount(1);
}
}
public function testParsesAnonMutationOperations()
{
/** @noinspection PhpUnhandledExceptionInspection */
parse(dedent('
mutation {
mutationField
}
'));
$this->addToAssertionCount(1);
}
public function testParsesAnonSubscriptionOperations()
{
/** @noinspection PhpUnhandledExceptionInspection */
parse(dedent('
subscription {
subscriptionField
}
'));
$this->addToAssertionCount(1);
}
public function testParsesNamedMutationOperations()
{
/** @noinspection PhpUnhandledExceptionInspection */
parse(dedent('
mutation Foo {
mutationField
}
'));
$this->addToAssertionCount(1);
}
public function testParsesNamedSubscriptionOperations()
{
/** @noinspection PhpUnhandledExceptionInspection */
parse(dedent('
subscription Foo {
subscriptionField
}
'));
$this->addToAssertionCount(1);
}
public function testCreatesAST()
{
/** @noinspection PhpUnhandledExceptionInspection */
$actual = parse(dedent('
{
node(id: 4) {
id,
name
}
}
'));
$this->assertEquals([
'kind' => NodeKindEnum::DOCUMENT,
'loc' => ['start' => 0, 'end' => 41],
'definitions' => [
[
'kind' => NodeKindEnum::OPERATION_DEFINITION,
'loc' => ['start' => 0, 'end' => 40],
'operation' => 'query',
'name' => null,
'variableDefinitions' => [],
'directives' => [],
'selectionSet' => [
'kind' => NodeKindEnum::SELECTION_SET,
'loc' => ['start' => 0, 'end' => 40],
'selections' => [
[
'kind' => NodeKindEnum::FIELD,
'loc' => ['start' => 4, 'end' => 38],
'alias' => null,
'name' => [
'kind' => NodeKindEnum::NAME,
'loc' => ['start' => 4, 'end' => 8],
'value' => 'node',
],
'arguments' => [
[
'kind' => NodeKindEnum::ARGUMENT,
'name' => [
'kind' => NodeKindEnum::NAME,
'loc' => ['start' => 9, 'end' => 11],
'value' => 'id',
],
'value' => [
'kind' => NodeKindEnum::INT,
'loc' => ['start' => 13, 'end' => 14],
'value' => '4',
],
'loc' => ['start' => 9, 'end' => 14],
],
],
'directives' => [],
'selectionSet' => [
'kind' => NodeKindEnum::SELECTION_SET,
'loc' => ['start' => 16, 'end' => 38],
'selections' => [
[
'kind' => NodeKindEnum::FIELD,
'loc' => ['start' => 22, 'end' => 24],
'alias' => null,
'name' => [
'kind' => NodeKindEnum::NAME,
'loc' => ['start' => 22, 'end' => 24],
'value' => 'id',
],
'arguments' => [],
'directives' => [],
'selectionSet' => null,
],
[
'kind' => NodeKindEnum::FIELD,
'loc' => ['start' => 30, 'end' => 34],
'alias' => null,
'name' => [
'kind' => NodeKindEnum::NAME,
'loc' => ['start' => 30, 'end' => 34],
'value' => 'name',
],
'arguments' => [],
'directives' => [],
'selectionSet' => null,
],
],
],
],
],
],
],
],
], $actual->toAST());
}
public function testCreatesAstFromNamelessQueryWithoutVariables()
{
/** @noinspection PhpUnhandledExceptionInspection */
$actual = parse(dedent('
query {
node {
id
}
}
'));
$this->assertEquals([
'kind' => NodeKindEnum::DOCUMENT,
'loc' => ['start' => 0, 'end' => 30],
'definitions' => [
[
'kind' => NodeKindEnum::OPERATION_DEFINITION,
'loc' => ['start' => 0, 'end' => 29],
'operation' => 'query',
'name' => null,
'variableDefinitions' => [],
'directives' => [],
'selectionSet' => [
'kind' => NodeKindEnum::SELECTION_SET,
'loc' => ['start' => 6, 'end' => 29],
'selections' => [
[
'kind' => NodeKindEnum::FIELD,
'loc' => ['start' => 10, 'end' => 27],
'alias' => null,
'name' => [
'kind' => NodeKindEnum::NAME,
'loc' => ['start' => 10, 'end' => 14],
'value' => 'node',
],
'arguments' => [],
'directives' => [],
'selectionSet' => [
'kind' => NodeKindEnum::SELECTION_SET,
'loc' => ['start' => 15, 'end' => 27],
'selections' => [
[
'kind' => NodeKindEnum::FIELD,
'loc' => ['start' => 21, 'end' => 23],
'alias' => null,
'name' => [
'kind' => NodeKindEnum::NAME,
'loc' => ['start' => 21, 'end' => 23],
'value' => 'id',
],
'arguments' => [],
'directives' => [],
'selectionSet' => null,
],
],
],
],
],
],
],
],
], $actual->toAST());
}
// TODO: Consider adding test for 'allows parsing without source location information'
// TODO: Consider adding test for 'Experimental: allows parsing fragment defined variables'
// TODO: Consider adding test for 'contains location information that only stringifys start/end'
// Skip 'contains references to source' (not provided by cpp parser)
// Skip 'contains references to start and end tokens' (not provided by cpp parser)
public function testParsesNullValue()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parseValue('null');
$this->assertEquals($node->toAST(), [
'kind' => NodeKindEnum::NULL,
'loc' => ['start' => 0, 'end' => 4],
]);
}
public function testParsesListValue()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parseValue('[123 "abc"]');
$this->assertEquals($node->toAST(), [
'kind' => NodeKindEnum::LIST,
'loc' => ['start' => 0, 'end' => 11],
'values' => [
[
'kind' => NodeKindEnum::INT,
'loc' => ['start' => 1, 'end' => 4],
'value' => '123',
],
[
'kind' => NodeKindEnum::STRING,
'loc' => ['start' => 5, 'end' => 10],
'block' => false,
'value' => 'abc',
],
],
]);
}
public function testParsesBlockStrings()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parseValue('["""long""" "short"]');
$this->assertEquals($node->toAST(), [
'kind' => NodeKindEnum::LIST,
'loc' => ['start' => 0, 'end' => 20],
'values' => [
[
'kind' => NodeKindEnum::STRING,
'loc' => ['start' => 1, 'end' => 11],
'value' => 'long',
'block' => true,
],
[
'kind' => NodeKindEnum::STRING,
'loc' => ['start' => 12, 'end' => 19],
'value' => 'short',
'block' => false,
],
],
]);
}
public function testParsesWellKnownTypes()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parseType('String');
$this->assertEquals($node->toAST(), [
'kind' => NodeKindEnum::NAMED_TYPE,
'loc' => ['start' => 0, 'end' => 6],
'name' => [
'kind' => NodeKindEnum::NAME,
'loc' => ['start' => 0, 'end' => 6],
'value' => 'String',
],
]);
}
public function testParsesCustomTypes()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parseType('MyType');
$this->assertEquals($node->toAST(), [
'kind' => NodeKindEnum::NAMED_TYPE,
'loc' => ['start' => 0, 'end' => 6],
'name' => [
'kind' => NodeKindEnum::NAME,
'loc' => ['start' => 0, 'end' => 6],
'value' => 'MyType',
],
]);
}
public function testParsesListTypes()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parseType('[MyType]');
$this->assertEquals($node->toAST(), [
'kind' => NodeKindEnum::LIST_TYPE,
'loc' => ['start' => 0, 'end' => 8],
'type' => [
'kind' => NodeKindEnum::NAMED_TYPE,
'loc' => ['start' => 1, 'end' => 7],
'name' => [
'kind' => NodeKindEnum::NAME,
'loc' => ['start' => 1, 'end' => 7],
'value' => 'MyType',
],
],
]);
}
public function testParsesNonNullTypes()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parseType('MyType!');
$this->assertEquals($node->toAST(), [
'kind' => NodeKindEnum::NON_NULL_TYPE,
'loc' => ['start' => 0, 'end' => 7],
'type' => [
'kind' => NodeKindEnum::NAMED_TYPE,
'loc' => ['start' => 0, 'end' => 6],
'name' => [
'kind' => NodeKindEnum::NAME,
'loc' => ['start' => 0, 'end' => 6],
'value' => 'MyType',
],
],
]);
}
public function testParsesNestedTypes()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parseType('[MyType!]');
$this->assertEquals($node->toAST(), [
'kind' => NodeKindEnum::LIST_TYPE,
'loc' => ['start' => 0, 'end' => 9],
'type' => [
'kind' => NodeKindEnum::NON_NULL_TYPE,
'loc' => ['start' => 1, 'end' => 8],
'type' => [
'kind' => NodeKindEnum::NAMED_TYPE,
'loc' => ['start' => 1, 'end' => 7],
'name' => [
'kind' => NodeKindEnum::NAME,
'loc' => ['start' => 1, 'end' => 7],
'value' => 'MyType',
],
],
],
]);
}
/**
* The purpose of this test case is that it should *not* crash with "Syntax Error: Cannot contain the invalid character "
* @throws SyntaxErrorException
* @throws \Digia\GraphQL\Error\InvariantException
*/
public function testParsesGitHubIssue253(): void
{
$document = parse(dedent('{
songs(first: 5, search: {name: "Vårt land"}) {
edges {
node {
name
}
}
}
}
'));
$this->addToAssertionCount(1);
}
}
================================================
FILE: tests/Functional/Language/SchemaParserTest.php
================================================
assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION,
'description' => null,
'name' => $this->nameNode('Hello', ['start' => 6, 'end' => 11]),
'interfaces' => [],
'directives' => [],
'fields' => [
$this->fieldNode(
$this->nameNode('world', ['start' => 16, 'end' => 21]),
$this->typeNode(TypeNameEnum::STRING, ['start' => 23, 'end' => 29]),
['start' => 16, 'end' => 29]
),
],
'loc' => ['start' => 1, 'end' => 31],
],
],
'loc' => ['start' => 0, 'end' => 31],
]), $node->toJSON());
}
public function testParsesWithDescriptionString()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
"Description"
type Hello {
world: String
}');
$this->assertArraySubset([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION,
'name' => $this->nameNode('Hello', ['start' => 20, 'end' => 25]),
'description' => [
'kind' => NodeKindEnum::STRING,
'value' => 'Description',
'loc' => ['start' => 1, 'end' => 14],
],
],
],
'loc' => ['start' => 0, 'end' => 45],
], $node->toArray());
}
public function testParsesWithDescriptionMultiLineString()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
"""
Description
"""
# Even with comments between them
type Hello {
world: String
}');
$this->assertArraySubset([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION,
'name' => $this->nameNode('Hello', ['start' => 60, 'end' => 65]),
'description' => [
'kind' => NodeKindEnum::STRING,
'value' => 'Description',
'loc' => ['start' => 1, 'end' => 20],
],
],
],
'loc' => ['start' => 0, 'end' => 85],
], $node->toArray());
}
public function testSimpleExtension()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
extend type Hello {
world: String
}');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::OBJECT_TYPE_EXTENSION,
'name' => $this->nameNode('Hello', ['start' => 13, 'end' => 18]),
'interfaces' => [],
'directives' => [],
'fields' => [
$this->fieldNode(
$this->nameNode('world', ['start' => 23, 'end' => 28]),
$this->typeNode(TypeNameEnum::STRING, ['start' => 30, 'end' => 36]),
['start' => 23, 'end' => 36]
),
],
'loc' => ['start' => 1, 'end' => 38],
],
],
'loc' => ['start' => 0, 'end' => 38],
]), $node->toJSON());
}
public function testExtensionWithoutFields()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('extend type Hello implements Greeting');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::OBJECT_TYPE_EXTENSION,
'name' => $this->nameNode('Hello', ['start' => 12, 'end' => 17]),
'interfaces' => [
$this->typeNode('Greeting', ['start' => 29, 'end' => 37]),
],
'directives' => [],
'fields' => [],
'loc' => ['start' => 0, 'end' => 37],
],
],
'loc' => ['start' => 0, 'end' => 37],
]), $node->toJSON());
}
public function testExtensionWithoutFieldsFollowedByExtension()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
extend type Hello implements Greeting
extend type Hello implements SecondGreeting
');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::OBJECT_TYPE_EXTENSION,
'name' => $this->nameNode('Hello', ['start' => 19, 'end' => 24]),
'interfaces' => [
$this->typeNode('Greeting', ['start' => 36, 'end' => 44]),
],
'directives' => [],
'fields' => [],
'loc' => ['start' => 7, 'end' => 44],
],
[
'kind' => NodeKindEnum::OBJECT_TYPE_EXTENSION,
'name' => $this->nameNode('Hello', ['start' => 64, 'end' => 69]),
'interfaces' => [
$this->typeNode('SecondGreeting', ['start' => 81, 'end' => 95]),
],
'directives' => [],
'fields' => [],
'loc' => ['start' => 52, 'end' => 95],
],
],
'loc' => ['start' => 0, 'end' => 100],
]), $node->toJSON());
}
public function testExtensionWithoutAnythingThrows()
{
$this->expectException(SyntaxErrorException::class);
$this->expectExceptionMessage('Unexpected ');
/** @noinspection PhpUnhandledExceptionInspection */
parse('extend type Hello');
}
public function testExtensionsDoNotIncludeDescriptions()
{
$this->expectException(SyntaxErrorException::class);
$this->expectExceptionMessage('Unexpected Name "extend"');
/** @noinspection PhpUnhandledExceptionInspection */
parse('
"Description"
extend type Hello {
world: String
}');
$this->expectException(SyntaxErrorException::class);
$this->expectExceptionMessage('Unexpected String "Description"');
/** @noinspection PhpUnhandledExceptionInspection */
parse('
extend "Description" type Hello {
world: String
}');
}
public function testSchemaExtension()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse(dedent('
extend schema {
mutation: Mutation
}
'));
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::SCHEMA_EXTENSION,
'directives' => [],
'operationTypes' => [
[
'kind' => NodeKindEnum::OPERATION_TYPE_DEFINITION,
'operation' => 'mutation',
'type' => $this->typeNode('Mutation', ['start' => 28, 'end' => 36]),
'loc' => ['start' => 18, 'end' => 36],
],
],
'loc' => ['start' => 0, 'end' => 38],
],
],
'loc' => ['start' => 0, 'end' => 39],
]), $node->toJSON());
}
public function testSchemaExtensionWithOnlyDirectives()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('extend schema @directive');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::SCHEMA_EXTENSION,
'directives' => [
[
'kind' => NodeKindEnum::DIRECTIVE,
'name' => $this->nameNode('directive', ['start' => 15, 'end' => 24]),
'arguments' => [],
'loc' => ['start' => 14, 'end' => 24],
],
],
'operationTypes' => [],
'loc' => ['start' => 0, 'end' => 24],
],
],
'loc' => ['start' => 0, 'end' => 24],
]), $node->toJSON());
}
public function testSimpleNonNullType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
type Hello {
world: String!
}');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION,
'description' => null,
'name' => $this->nameNode('Hello', ['start' => 6, 'end' => 11]),
'interfaces' => [],
'directives' => [],
'fields' => [
$this->fieldNode(
$this->nameNode('world', ['start' => 16, 'end' => 21]),
[
'kind' => NodeKindEnum::NON_NULL_TYPE,
'type' => $this->typeNode(TypeNameEnum::STRING, ['start' => 23, 'end' => 29]),
'loc' => ['start' => 23, 'end' => 30],
],
['start' => 16, 'end' => 30]
),
],
'loc' => ['start' => 1, 'end' => 32],
],
],
'loc' => ['start' => 0, 'end' => 32],
]), $node->toJSON());
}
public function testSimpleTypeInheritingInterface()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('type Hello implements World { field: String }');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION,
'description' => null,
'name' => $this->nameNode('Hello', ['start' => 5, 'end' => 10]),
'interfaces' => [
$this->typeNode('World', ['start' => 22, 'end' => 27]),
],
'directives' => [],
'fields' => [
$this->fieldNode(
$this->nameNode('field', ['start' => 30, 'end' => 35]),
$this->typeNode(TypeNameEnum::STRING, ['start' => 37, 'end' => 43]),
['start' => 30, 'end' => 43]
),
],
'loc' => ['start' => 0, 'end' => 45],
],
],
'loc' => ['start' => 0, 'end' => 45],
]), $node->toJSON());
}
public function testSimpleTypeInheritingMultipleInterface()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('type Hello implements Wo & rld { field: String }');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION,
'description' => null,
'name' => $this->nameNode('Hello', ['start' => 5, 'end' => 10]),
'interfaces' => [
$this->typeNode('Wo', ['start' => 22, 'end' => 24]),
$this->typeNode('rld', ['start' => 27, 'end' => 30]),
],
'directives' => [],
'fields' => [
$this->fieldNode(
$this->nameNode('field', ['start' => 33, 'end' => 38]),
$this->typeNode(TypeNameEnum::STRING, ['start' => 40, 'end' => 46]),
['start' => 33, 'end' => 46]
),
],
'loc' => ['start' => 0, 'end' => 48],
],
],
'loc' => ['start' => 0, 'end' => 48],
]), $node->toJSON());
}
public function testSimpleTypeInheritingMultipleInterfaceWithLeadingAmpersand()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('type Hello implements & Wo & rld { field: String }');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION,
'description' => null,
'name' => $this->nameNode('Hello', ['start' => 5, 'end' => 10]),
'interfaces' => [
$this->typeNode('Wo', ['start' => 24, 'end' => 26]),
$this->typeNode('rld', ['start' => 29, 'end' => 32]),
],
'directives' => [],
'fields' => [
$this->fieldNode(
$this->nameNode('field', ['start' => 35, 'end' => 40]),
$this->typeNode(TypeNameEnum::STRING, ['start' => 42, 'end' => 48]),
['start' => 35, 'end' => 48]
),
],
'loc' => ['start' => 0, 'end' => 50],
],
],
'loc' => ['start' => 0, 'end' => 50],
]), $node->toJSON());
}
public function testSingleValueEnum()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('enum Hello { WORLD }');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::ENUM_TYPE_DEFINITION,
'description' => null,
'name' => $this->nameNode('Hello', ['start' => 5, 'end' => 10]),
'directives' => [],
'values' => [
$this->enumValueNode('WORLD', ['start' => 13, 'end' => 18]),
],
'loc' => ['start' => 0, 'end' => 20],
],
],
'loc' => ['start' => 0, 'end' => 20],
]), $node->toJSON());
}
public function testDoubleValueEnum()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('enum Hello { WO, RLD }');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::ENUM_TYPE_DEFINITION,
'description' => null,
'name' => $this->nameNode('Hello', ['start' => 5, 'end' => 10]),
'directives' => [],
'values' => [
$this->enumValueNode('WO', ['start' => 13, 'end' => 15]),
$this->enumValueNode('RLD', ['start' => 17, 'end' => 20]),
],
'loc' => ['start' => 0, 'end' => 22],
],
],
'loc' => ['start' => 0, 'end' => 22],
]), $node->toJSON());
}
public function testSimpleInterface()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
interface Hello {
world: String
}');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::INTERFACE_TYPE_DEFINITION,
'description' => null,
'name' => $this->nameNode('Hello', ['start' => 11, 'end' => 16]),
'directives' => [],
'fields' => [
$this->fieldNode(
$this->nameNode('world', ['start' => 21, 'end' => 26]),
$this->typeNode(TypeNameEnum::STRING, ['start' => 28, 'end' => 34]),
['start' => 21, 'end' => 34]
),
],
'loc' => ['start' => 1, 'end' => 36],
],
],
'loc' => ['start' => 0, 'end' => 36],
]), $node->toJSON());
}
public function parseSimpleFieldWithArgument()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
type Hello {
world(flag: Boolean): String
}');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION,
'description' => null,
'name' => $this->nameNode('Hello', ['start' => 6, 'end' => 11]),
'interfaces' => [],
'directives' => [],
'fields' => [
$this->fieldNodeWithArguments(
$this->nameNode('world', ['start' => 16, 'end' => 21]),
$this->typeNode(TypeNameEnum::STRING, ['start' => 45, 'end' => 51]),
[
$this->inputValueNode(
$this->nameNode('flag', ['start' => 22, 'end' => 26]),
$this->typeNode(TypeNameEnum::BOOLEAN, ['start' => 28, 'end' => 35]),
null,
['start' => 22, 'end' => 35]
),
],
['start' => 16, 'end' => 44]
),
],
'loc' => ['start' => 1, 'end' => 46],
],
],
'loc' => ['start' => 0, 'end' => 46],
]), $node->toJSON());
}
public function testSimpleFieldWithArgumentWithDefaultValue()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
type Hello {
world(flag: Boolean = true): String
}');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION,
'description' => null,
'name' => $this->nameNode('Hello', ['start' => 6, 'end' => 11]),
'interfaces' => [],
'directives' => [],
'fields' => [
$this->fieldNodeWithArguments(
$this->nameNode('world', ['start' => 16, 'end' => 21]),
$this->typeNode(TypeNameEnum::STRING, ['start' => 45, 'end' => 51]),
[
$this->inputValueNode(
$this->nameNode('flag', ['start' => 22, 'end' => 26]),
$this->typeNode(TypeNameEnum::BOOLEAN, ['start' => 28, 'end' => 35]),
[
'kind' => NodeKindEnum::BOOLEAN,
'value' => true,
'loc' => ['start' => 38, 'end' => 42],
],
['start' => 22, 'end' => 42]
),
],
['start' => 16, 'end' => 51]
),
],
'loc' => ['start' => 1, 'end' => 53],
],
],
'loc' => ['start' => 0, 'end' => 53],
]), $node->toJSON());
}
public function testSimpleFieldWithListArgument()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
type Hello {
world(things: [String]): String
}');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION,
'description' => null,
'name' => $this->nameNode('Hello', ['start' => 6, 'end' => 11]),
'interfaces' => [],
'directives' => [],
'fields' => [
$this->fieldNodeWithArguments(
$this->nameNode('world', ['start' => 16, 'end' => 21]),
$this->typeNode(TypeNameEnum::STRING, ['start' => 41, 'end' => 47]),
[
$this->inputValueNode(
$this->nameNode('things', ['start' => 22, 'end' => 28]),
[
'kind' => NodeKindEnum::LIST_TYPE,
'type' => $this->typeNode(TypeNameEnum::STRING, ['start' => 31, 'end' => 37]),
'loc' => ['start' => 30, 'end' => 38],
],
null,
['start' => 22, 'end' => 38]
),
],
['start' => 16, 'end' => 47]
),
],
'loc' => ['start' => 1, 'end' => 49],
],
],
'loc' => ['start' => 0, 'end' => 49],
]), $node->toJSON());
}
public function parseSimpleFieldWithTwoArguments()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
type Hello {
world(argOne: Boolean, argTwo: Int): String
}');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION,
'description' => null,
'name' => $this->nameNode('Hello', ['start' => 6, 'end' => 11]),
'interfaces' => [],
'directives' => [],
'fields' => [
$this->fieldNodeWithArguments(
$this->nameNode('world', ['start' => 16, 'end' => 21]),
$this->typeNode(TypeNameEnum::STRING, ['start' => 53, 'end' => 59]),
[
$this->inputValueNode(
$this->nameNode('argOne', ['start' => 22, 'end' => 28]),
$this->typeNode(TypeNameEnum::BOOLEAN, ['start' => 30, 'end' => 37]),
null,
['start' => 22, 'end' => 37]
),
$this->inputValueNode(
$this->nameNode('argTwo', ['start' => 39, 'end' => 45]),
$this->typeNode(TypeNameEnum::INT, ['start' => 47, 'end' => 50]),
null,
['start' => 39, 'end' => 50]
),
],
['start' => 16, 'end' => 59]
),
],
'loc' => ['start' => 1, 'end' => 61],
],
],
'loc' => ['start' => 0, 'end' => 61],
]), $node->toJSON());
}
public function testSimpleUnion()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('union Hello = World');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::UNION_TYPE_DEFINITION,
'description' => null,
'name' => $this->nameNode('Hello', ['start' => 6, 'end' => 11]),
'directives' => [],
'types' => [
$this->typeNode('World', ['start' => 14, 'end' => 19]),
],
'loc' => ['start' => 0, 'end' => 19],
],
],
'loc' => ['start' => 0, 'end' => 19],
]), $node->toJSON());
}
public function testSimpleUnionWithTypes()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('union Hello = Wo | Rld');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::UNION_TYPE_DEFINITION,
'description' => null,
'name' => $this->nameNode('Hello', ['start' => 6, 'end' => 11]),
'directives' => [],
'types' => [
$this->typeNode('Wo', ['start' => 14, 'end' => 16]),
$this->typeNode('Rld', ['start' => 19, 'end' => 22]),
],
'loc' => ['start' => 0, 'end' => 22],
],
],
'loc' => ['start' => 0, 'end' => 22],
]), $node->toJSON());
}
public function testSimpleUnionWithTypesAndLeadingPipe()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('union Hello = | Wo | Rld');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::UNION_TYPE_DEFINITION,
'description' => null,
'name' => $this->nameNode('Hello', ['start' => 6, 'end' => 11]),
'directives' => [],
'types' => [
$this->typeNode('Wo', ['start' => 16, 'end' => 18]),
$this->typeNode('Rld', ['start' => 21, 'end' => 24]),
],
'loc' => ['start' => 0, 'end' => 24],
],
],
'loc' => ['start' => 0, 'end' => 24],
]), $node->toJSON());
}
public function testUnionFailsWithNoTypes()
{
$this->expectException(SyntaxErrorException::class);
$this->expectExceptionMessage('Expected Name, found ');
/** @noinspection PhpUnhandledExceptionInspection */
parse('union Hello = |');
}
public function testUnionFailsWithLeadingDoublePipe()
{
$this->expectException(SyntaxErrorException::class);
$this->expectExceptionMessage('Expected Name, found |');
/** @noinspection PhpUnhandledExceptionInspection */
parse('union Hello = || Wo | Rld');
}
public function testUnionFailsWithTrailingPipe()
{
$this->expectException(SyntaxErrorException::class);
$this->expectExceptionMessage('Expected Name, found ');
/** @noinspection PhpUnhandledExceptionInspection */
parse('union Hello = | Wo | Rld |');
}
public function testSimpleScalar()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('scalar Hello');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::SCALAR_TYPE_DEFINITION,
'description' => null,
'name' => $this->nameNode('Hello', ['start' => 7, 'end' => 12]),
'directives' => [],
'loc' => ['start' => 0, 'end' => 12],
],
],
'loc' => ['start' => 0, 'end' => 12],
]), $node->toJSON());
}
public function testSimpleInputObject()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
input Hello {
world: String
}');
$this->assertEquals(jsonEncode([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
[
'kind' => NodeKindEnum::INPUT_OBJECT_TYPE_DEFINITION,
'description' => null,
'name' => $this->nameNode('Hello', ['start' => 7, 'end' => 12]),
'directives' => [],
'fields' => [
$this->inputValueNode(
$this->nameNode('world', ['start' => 17, 'end' => 22]),
$this->typeNode(TypeNameEnum::STRING, ['start' => 24, 'end' => 30]),
null,
['start' => 17, 'end' => 30]
),
],
'loc' => ['start' => 1, 'end' => 32],
],
],
'loc' => ['start' => 0, 'end' => 32],
]), $node->toJSON());
}
public function testSimpleInputObjectWithArgumentsShouldFail()
{
$this->expectException(SyntaxErrorException::class);
$this->expectExceptionMessage('Expected :, found (');
/** @noinspection PhpUnhandledExceptionInspection */
parse('
input Hello {
world(foo: Int): String
}');
}
public function testDirectiveWithIncorrectLocations()
{
$this->expectException(SyntaxErrorException::class);
$this->expectExceptionMessage('Unexpected Name "INCORRECT_LOCATION"');
/** @noinspection PhpUnhandledExceptionInspection */
parse('
directive @foo on FIELD | INCORRECT_LOCATION');
}
protected function typeNode($name, $loc)
{
return [
'kind' => NodeKindEnum::NAMED_TYPE,
'name' => $this->nameNode($name, $loc),
'loc' => $loc,
];
}
protected function nameNode($name, $loc)
{
return [
'kind' => NodeKindEnum::NAME,
'value' => $name,
'loc' => $loc,
];
}
protected function fieldNode($name, $type, $loc)
{
return $this->fieldNodeWithArguments($name, $type, [], $loc);
}
protected function fieldNodeWithArguments($name, $type, $arguments, $loc)
{
return [
'kind' => NodeKindEnum::FIELD_DEFINITION,
'description' => null,
'name' => $name,
'arguments' => $arguments,
'type' => $type,
'directives' => [],
'loc' => $loc,
];
}
protected function enumValueNode($name, $loc)
{
return [
'kind' => NodeKindEnum::ENUM_VALUE_DEFINITION,
'description' => null,
'name' => $this->nameNode($name, $loc),
'directives' => [],
'loc' => $loc,
];
}
protected function inputValueNode($name, $type, $defaultValue, $loc)
{
return [
'kind' => NodeKindEnum::INPUT_VALUE_DEFINITION,
'description' => null,
'name' => $name,
'type' => $type,
'defaultValue' => $defaultValue,
'directives' => [],
'loc' => $loc,
];
}
}
================================================
FILE: tests/Functional/Language/SchemaPrinterTest.php
================================================
toJSON();
printNode($ast);
$this->assertEquals($ast->toJSON(), $astBefore);
}
}
================================================
FILE: tests/Functional/Language/StringSourceBuilderTest.php
================================================
build();
$this->assertGreaterThan(0, $source->getBodyLength());
}
}
================================================
FILE: tests/Functional/Language/VisitorTest.php
================================================
getVisitorInfo()->getPath(), 0)];
return new VisitorResult($node);
},
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = ['leave', array_slice($node->getVisitorInfo()->getPath(), 0)];
return new VisitorResult($node);
}
);
/** @noinspection PhpUnhandledExceptionInspection */
$ast->acceptVisitor(new VisitorInfo($visitor));
$this->assertEquals([
['enter', []],
['enter', ['definitions', 0]],
['enter', ['definitions', 0, 'selectionSet']],
['enter', ['definitions', 0, 'selectionSet', 'selections', 0]],
['enter', ['definitions', 0, 'selectionSet', 'selections', 0, 'name']],
['leave', ['definitions', 0, 'selectionSet', 'selections', 0, 'name']],
['leave', ['definitions', 0, 'selectionSet', 'selections', 0]],
['leave', ['definitions', 0, 'selectionSet']],
['leave', ['definitions', 0]],
['leave', []],
], $visited);
}
public function testAllowsForEditingOnEnter()
{
/** @noinspection PhpUnhandledExceptionInspection */
$document = parse('{ a, b, c { a, b, c } }', ['noLocation' => true]);
$visitor = new Visitor(
function (NodeInterface $node): VisitorResult {
if ($node instanceof FieldNode && $node->getNameValue() === 'b') {
return new VisitorResult(null, VisitorResult::ACTION_REPLACE);
}
return new VisitorResult($node);
}
);
/** @noinspection PhpUnhandledExceptionInspection */
$editedDocument = $document->acceptVisitor(new VisitorInfo($visitor));
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals(
parse('{ a, b, c { a, b, c } }', ['noLocation' => true])->toArray(),
$document->toAST()
);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals(
parse('{ a, c { a, c } }', ['noLocation' => true])->toArray(),
$editedDocument->toAST()
);
}
public function testAllowsForEditingOnLeave()
{
/** @noinspection PhpUnhandledExceptionInspection */
$document = parse('{ a, b, c { a, b, c } }', ['noLocation' => true]);
$visitor = new Visitor(
null,
function (NodeInterface $node): VisitorResult {
if ($node instanceof FieldNode && $node->getNameValue() === 'b') {
return new VisitorResult(null, VisitorResult::ACTION_REPLACE);
}
return new VisitorResult($node);
}
);
/** @noinspection PhpUnhandledExceptionInspection */
$editedDocument = $document->acceptVisitor(new VisitorInfo($visitor));
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals(
parse('{ a, b, c { a, b, c } }', ['noLocation' => true])->toArray(),
$document->toAST()
);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals(
parse('{ a, c { a, c } }', ['noLocation' => true])->toArray(),
$editedDocument->toAST()
);
}
public function testVisitsEditedNode()
{
$addedField = new AddedFieldNode(
null,
new NameNode('__typename', null),
[],
[],
null,
null
);
$didVisitEditedNode = false;
/** @noinspection PhpUnhandledExceptionInspection */
$ast = parse('{ a { x } }', ['noLocation' => true]);
$visitor = new Visitor(
function (NodeInterface $node) use (&$didVisitEditedNode, $addedField): VisitorResult {
if ($node instanceof FieldNode && $node->getNameValue() === 'a') {
return new VisitorResult($addedField);
}
if ($node instanceof AddedFieldNode) {
$didVisitEditedNode = true;
}
return new VisitorResult($node);
}
);
/** @noinspection PhpUnhandledExceptionInspection */
$ast->acceptVisitor(new VisitorInfo($visitor));
$this->assertTrue($didVisitEditedNode);
}
public function testAllowsSkippingSubTree()
{
$visited = [];
/** @noinspection PhpUnhandledExceptionInspection */
$ast = parse('{ a, b { x }, c }', ['noLocation' => true]);
$visitor = new Visitor(
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = ['enter', $node->getKind(), $node instanceof NameNode ? $node->getValue() : null];
if ($node instanceof FieldNode && $node->getNameValue() === 'b') {
return new VisitorResult(null);
}
return new VisitorResult($node);
},
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = ['leave', $node->getKind(), $node instanceof NameNode ? $node->getValue() : null];
return new VisitorResult($node);
}
);
/** @noinspection PhpUnhandledExceptionInspection */
$ast->acceptVisitor(new VisitorInfo($visitor));
$this->assertEquals([
['enter', 'Document', null],
['enter', 'OperationDefinition', null],
['enter', 'SelectionSet', null],
['enter', 'Field', null],
['enter', 'Name', 'a'],
['leave', 'Name', 'a'],
['leave', 'Field', null],
['enter', 'Field', null],
['enter', 'Field', null],
['enter', 'Name', 'c'],
['leave', 'Name', 'c'],
['leave', 'Field', null],
['leave', 'SelectionSet', null],
['leave', 'OperationDefinition', null],
['leave', 'Document', null],
], $visited);
}
public function testAllowsEarlyExitWhileVisiting()
{
$visited = [];
/** @noinspection PhpUnhandledExceptionInspection */
$ast = parse('{ a, b { x }, c }', ['noLocation' => true]);
$visitor = new Visitor(
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = ['enter', $node->getKind(), $node instanceof NameNode ? $node->getValue() : null];
if ($node instanceof NameNode && $node->getValue() === 'x') {
return new VisitorResult($node, VisitorResult::ACTION_BREAK);
}
return new VisitorResult($node);
},
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = ['leave', $node->getKind(), $node instanceof NameNode ? $node->getValue() : null];
return new VisitorResult($node);
}
);
try {
$ast->acceptVisitor(new VisitorInfo($visitor));
} catch (VisitorBreak $e) {
}
$this->assertEquals([
['enter', 'Document', null],
['enter', 'OperationDefinition', null],
['enter', 'SelectionSet', null],
['enter', 'Field', null],
['enter', 'Name', 'a'],
['leave', 'Name', 'a'],
['leave', 'Field', null],
['enter', 'Field', null],
['enter', 'Name', 'b'],
['leave', 'Name', 'b'],
['enter', 'SelectionSet', null],
['enter', 'Field', null],
['enter', 'Name', 'x'],
], $visited);
}
public function testAllowsEarlyExitWhileLeaving()
{
$visited = [];
/** @noinspection PhpUnhandledExceptionInspection */
$ast = parse('{ a, b { x }, c }', ['noLocation' => true]);
$visitor = new Visitor(
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = ['enter', $node->getKind(), $node instanceof NameNode ? $node->getValue() : null];
return new VisitorResult($node);
},
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = ['leave', $node->getKind(), $node instanceof NameNode ? $node->getValue() : null];
if ($node instanceof NameNode && $node->getValue() === 'x') {
return new VisitorResult($node, VisitorResult::ACTION_BREAK);
}
return new VisitorResult($node);
}
);
try {
$ast->acceptVisitor(new VisitorInfo($visitor));
} catch (VisitorBreak $e) {
}
$this->assertEquals([
['enter', 'Document', null],
['enter', 'OperationDefinition', null],
['enter', 'SelectionSet', null],
['enter', 'Field', null],
['enter', 'Name', 'a'],
['leave', 'Name', 'a'],
['leave', 'Field', null],
['enter', 'Field', null],
['enter', 'Name', 'b'],
['leave', 'Name', 'b'],
['enter', 'SelectionSet', null],
['enter', 'Field', null],
['enter', 'Name', 'x'],
['leave', 'Name', 'x'],
], $visited);
}
public function testAllowsAKindVisitor()
{
$visited = [];
/** @noinspection PhpUnhandledExceptionInspection */
$ast = parse('{ a, b { x }, c }', ['noLocation' => true]);
$visitor = new Visitor(
function (NodeInterface $node) use (&$visited): VisitorResult {
if ($node instanceof NameNode || $node instanceof SelectionSetNode) {
$visited[] = ['enter', $node->getKind(), $node instanceof NameNode ? $node->getValue() : null];
}
return new VisitorResult($node);
},
function (NodeInterface $node) use (&$visited): VisitorResult {
if ($node instanceof SelectionSetNode) {
$visited[] = ['leave', $node->getKind(), $node instanceof NameNode ? $node->getValue() : null];
}
return new VisitorResult($node);
}
);
try {
$ast->acceptVisitor(new VisitorInfo($visitor));
} catch (VisitorBreak $e) {
}
$this->assertEquals([
['enter', 'SelectionSet', null],
['enter', 'Name', 'a'],
['enter', 'Name', 'b'],
['enter', 'SelectionSet', null],
['enter', 'Name', 'x'],
['leave', 'SelectionSet', null],
['enter', 'Name', 'c'],
['leave', 'SelectionSet', null],
], $visited);
}
public function testVisitsVariablesDefinedInFragments()
{
$visited = [];
/** @noinspection PhpUnhandledExceptionInspection */
$ast = parse('fragment a($v: Boolean = false) on t { f }', ['noLocation' => true]);
$visitor = new Visitor(
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = ['enter', $node->getKind(), $node instanceof NameNode ? $node->getValue() : null];
return new VisitorResult($node);
},
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = ['leave', $node->getKind(), $node instanceof NameNode ? $node->getValue() : null];
return new VisitorResult($node);
}
);
try {
$ast->acceptVisitor(new VisitorInfo($visitor));
} catch (VisitorBreak $e) {
}
$this->assertEquals([
['enter', 'Document', null],
['enter', 'FragmentDefinition', null],
['enter', 'Name', 'a'],
['leave', 'Name', 'a'],
['enter', 'VariableDefinition', null],
['enter', 'Variable', null],
['enter', 'Name', 'v'],
['leave', 'Name', 'v'],
['leave', 'Variable', null],
['enter', 'NamedType', null],
['enter', 'Name', 'Boolean'],
['leave', 'Name', 'Boolean'],
['leave', 'NamedType', null],
['enter', 'BooleanValue', false],
['leave', 'BooleanValue', false],
['leave', 'VariableDefinition', null],
['enter', 'NamedType', null],
['enter', 'Name', 't'],
['leave', 'Name', 't'],
['leave', 'NamedType', null],
['enter', 'SelectionSet', null],
['enter', 'Field', null],
['enter', 'Name', 'f'],
['leave', 'Name', 'f'],
['leave', 'Field', null],
['leave', 'SelectionSet', null],
['leave', 'FragmentDefinition', null],
['leave', 'Document', null],
], $visited);
}
public function testVisitsKitchenSink()
{
$visited = [];
$kitchenSink = readFileContents(__DIR__ . '/kitchen-sink.graphql');
/** @noinspection PhpUnhandledExceptionInspection */
$ast = parse($kitchenSink);
$visitor = new Visitor(
function (NodeInterface $node) use (&$visited): VisitorResult {
$key = $node->getVisitorInfo()->getKey();
$parent = $node->getVisitorInfo()->getParent();
$visited[] = ['enter', $node->getKind(), $key, $parent ? $parent->getKind() : null];
return new VisitorResult($node);
},
function (NodeInterface $node) use (&$visited): VisitorResult {
$key = $node->getVisitorInfo()->getKey();
$parent = $node->getVisitorInfo()->getParent();
$visited[] = ['leave', $node->getKind(), $key, $parent ? $parent->getKind() : null];
return new VisitorResult($node);
}
);
try {
$ast->acceptVisitor(new VisitorInfo($visitor));
} catch (VisitorBreak $e) {
}
$this->assertEquals([
['enter', 'Document', null, null],
['enter', 'OperationDefinition', 0, null],
['enter', 'Name', 'name', 'OperationDefinition'],
['leave', 'Name', 'name', 'OperationDefinition'],
['enter', 'VariableDefinition', 0, null],
['enter', 'Variable', 'variable', 'VariableDefinition'],
['enter', 'Name', 'name', 'Variable'],
['leave', 'Name', 'name', 'Variable'],
['leave', 'Variable', 'variable', 'VariableDefinition'],
['enter', 'NamedType', 'type', 'VariableDefinition'],
['enter', 'Name', 'name', 'NamedType'],
['leave', 'Name', 'name', 'NamedType'],
['leave', 'NamedType', 'type', 'VariableDefinition'],
['leave', 'VariableDefinition', 0, null],
['enter', 'VariableDefinition', 1, null],
['enter', 'Variable', 'variable', 'VariableDefinition'],
['enter', 'Name', 'name', 'Variable'],
['leave', 'Name', 'name', 'Variable'],
['leave', 'Variable', 'variable', 'VariableDefinition'],
['enter', 'NamedType', 'type', 'VariableDefinition'],
['enter', 'Name', 'name', 'NamedType'],
['leave', 'Name', 'name', 'NamedType'],
['leave', 'NamedType', 'type', 'VariableDefinition'],
['enter', 'EnumValue', 'defaultValue', 'VariableDefinition'],
['leave', 'EnumValue', 'defaultValue', 'VariableDefinition'],
['leave', 'VariableDefinition', 1, null],
['enter', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
['enter', 'Field', 0, null],
['enter', 'Name', 'alias', 'Field'],
['leave', 'Name', 'alias', 'Field'],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['enter', 'Argument', 0, null],
['enter', 'Name', 'name', 'Argument'],
['leave', 'Name', 'name', 'Argument'],
['enter', 'ListValue', 'value', 'Argument'],
['enter', 'IntValue', 0, null],
['leave', 'IntValue', 0, null],
['enter', 'IntValue', 1, null],
['leave', 'IntValue', 1, null],
['leave', 'ListValue', 'value', 'Argument'],
['leave', 'Argument', 0, null],
['enter', 'SelectionSet', 'selectionSet', 'Field'],
['enter', 'Field', 0, null],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['leave', 'Field', 0, null],
['enter', 'InlineFragment', 1, null],
['enter', 'NamedType', 'typeCondition', 'InlineFragment'],
['enter', 'Name', 'name', 'NamedType'],
['leave', 'Name', 'name', 'NamedType'],
['leave', 'NamedType', 'typeCondition', 'InlineFragment'],
['enter', 'Directive', 0, null],
['enter', 'Name', 'name', 'Directive'],
['leave', 'Name', 'name', 'Directive'],
['leave', 'Directive', 0, null],
['enter', 'SelectionSet', 'selectionSet', 'InlineFragment'],
['enter', 'Field', 0, null],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['enter', 'SelectionSet', 'selectionSet', 'Field'],
['enter', 'Field', 0, null],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['leave', 'Field', 0, null],
['enter', 'Field', 1, null],
['enter', 'Name', 'alias', 'Field'],
['leave', 'Name', 'alias', 'Field'],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['enter', 'Argument', 0, null],
['enter', 'Name', 'name', 'Argument'],
['leave', 'Name', 'name', 'Argument'],
['enter', 'IntValue', 'value', 'Argument'],
['leave', 'IntValue', 'value', 'Argument'],
['leave', 'Argument', 0, null],
['enter', 'Argument', 1, null],
['enter', 'Name', 'name', 'Argument'],
['leave', 'Name', 'name', 'Argument'],
['enter', 'Variable', 'value', 'Argument'],
['enter', 'Name', 'name', 'Variable'],
['leave', 'Name', 'name', 'Variable'],
['leave', 'Variable', 'value', 'Argument'],
['leave', 'Argument', 1, null],
['enter', 'Directive', 0, null],
['enter', 'Name', 'name', 'Directive'],
['leave', 'Name', 'name', 'Directive'],
['enter', 'Argument', 0, null],
['enter', 'Name', 'name', 'Argument'],
['leave', 'Name', 'name', 'Argument'],
['enter', 'Variable', 'value', 'Argument'],
['enter', 'Name', 'name', 'Variable'],
['leave', 'Name', 'name', 'Variable'],
['leave', 'Variable', 'value', 'Argument'],
['leave', 'Argument', 0, null],
['leave', 'Directive', 0, null],
['enter', 'SelectionSet', 'selectionSet', 'Field'],
['enter', 'Field', 0, null],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['leave', 'Field', 0, null],
['enter', 'FragmentSpread', 1, null],
['enter', 'Name', 'name', 'FragmentSpread'],
['leave', 'Name', 'name', 'FragmentSpread'],
['leave', 'FragmentSpread', 1, null],
['leave', 'SelectionSet', 'selectionSet', 'Field'],
['leave', 'Field', 1, null],
['leave', 'SelectionSet', 'selectionSet', 'Field'],
['leave', 'Field', 0, null],
['leave', 'SelectionSet', 'selectionSet', 'InlineFragment'],
['leave', 'InlineFragment', 1, null],
['enter', 'InlineFragment', 2, null],
['enter', 'Directive', 0, null],
['enter', 'Name', 'name', 'Directive'],
['leave', 'Name', 'name', 'Directive'],
['enter', 'Argument', 0, null],
['enter', 'Name', 'name', 'Argument'],
['leave', 'Name', 'name', 'Argument'],
['enter', 'Variable', 'value', 'Argument'],
['enter', 'Name', 'name', 'Variable'],
['leave', 'Name', 'name', 'Variable'],
['leave', 'Variable', 'value', 'Argument'],
['leave', 'Argument', 0, null],
['leave', 'Directive', 0, null],
['enter', 'SelectionSet', 'selectionSet', 'InlineFragment'],
['enter', 'Field', 0, null],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['leave', 'Field', 0, null],
['leave', 'SelectionSet', 'selectionSet', 'InlineFragment'],
['leave', 'InlineFragment', 2, null],
['enter', 'InlineFragment', 3, null],
['enter', 'SelectionSet', 'selectionSet', 'InlineFragment'],
['enter', 'Field', 0, null],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['leave', 'Field', 0, null],
['leave', 'SelectionSet', 'selectionSet', 'InlineFragment'],
['leave', 'InlineFragment', 3, null],
['leave', 'SelectionSet', 'selectionSet', 'Field'],
['leave', 'Field', 0, null],
['leave', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
['leave', 'OperationDefinition', 0, null],
['enter', 'OperationDefinition', 1, null],
['enter', 'Name', 'name', 'OperationDefinition'],
['leave', 'Name', 'name', 'OperationDefinition'],
['enter', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
['enter', 'Field', 0, null],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['enter', 'Argument', 0, null],
['enter', 'Name', 'name', 'Argument'],
['leave', 'Name', 'name', 'Argument'],
['enter', 'IntValue', 'value', 'Argument'],
['leave', 'IntValue', 'value', 'Argument'],
['leave', 'Argument', 0, null],
['enter', 'Directive', 0, null],
['enter', 'Name', 'name', 'Directive'],
['leave', 'Name', 'name', 'Directive'],
['leave', 'Directive', 0, null],
['enter', 'SelectionSet', 'selectionSet', 'Field'],
['enter', 'Field', 0, null],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['enter', 'SelectionSet', 'selectionSet', 'Field'],
['enter', 'Field', 0, null],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['leave', 'Field', 0, null],
['leave', 'SelectionSet', 'selectionSet', 'Field'],
['leave', 'Field', 0, null],
['leave', 'SelectionSet', 'selectionSet', 'Field'],
['leave', 'Field', 0, null],
['leave', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
['leave', 'OperationDefinition', 1, null],
['enter', 'OperationDefinition', 2, null],
['enter', 'Name', 'name', 'OperationDefinition'],
['leave', 'Name', 'name', 'OperationDefinition'],
['enter', 'VariableDefinition', 0, null],
['enter', 'Variable', 'variable', 'VariableDefinition'],
['enter', 'Name', 'name', 'Variable'],
['leave', 'Name', 'name', 'Variable'],
['leave', 'Variable', 'variable', 'VariableDefinition'],
['enter', 'NamedType', 'type', 'VariableDefinition'],
['enter', 'Name', 'name', 'NamedType'],
['leave', 'Name', 'name', 'NamedType'],
['leave', 'NamedType', 'type', 'VariableDefinition'],
['leave', 'VariableDefinition', 0, null],
['enter', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
['enter', 'Field', 0, null],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['enter', 'Argument', 0, null],
['enter', 'Name', 'name', 'Argument'],
['leave', 'Name', 'name', 'Argument'],
['enter', 'Variable', 'value', 'Argument'],
['enter', 'Name', 'name', 'Variable'],
['leave', 'Name', 'name', 'Variable'],
['leave', 'Variable', 'value', 'Argument'],
['leave', 'Argument', 0, null],
['enter', 'SelectionSet', 'selectionSet', 'Field'],
['enter', 'Field', 0, null],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['enter', 'SelectionSet', 'selectionSet', 'Field'],
['enter', 'Field', 0, null],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['enter', 'SelectionSet', 'selectionSet', 'Field'],
['enter', 'Field', 0, null],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['leave', 'Field', 0, null],
['leave', 'SelectionSet', 'selectionSet', 'Field'],
['leave', 'Field', 0, null],
['enter', 'Field', 1, null],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['enter', 'SelectionSet', 'selectionSet', 'Field'],
['enter', 'Field', 0, null],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['leave', 'Field', 0, null],
['leave', 'SelectionSet', 'selectionSet', 'Field'],
['leave', 'Field', 1, null],
['leave', 'SelectionSet', 'selectionSet', 'Field'],
['leave', 'Field', 0, null],
['leave', 'SelectionSet', 'selectionSet', 'Field'],
['leave', 'Field', 0, null],
['leave', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
['leave', 'OperationDefinition', 2, null],
['enter', 'FragmentDefinition', 3, null],
['enter', 'Name', 'name', 'FragmentDefinition'],
['leave', 'Name', 'name', 'FragmentDefinition'],
['enter', 'NamedType', 'typeCondition', 'FragmentDefinition'],
['enter', 'Name', 'name', 'NamedType'],
['leave', 'Name', 'name', 'NamedType'],
['leave', 'NamedType', 'typeCondition', 'FragmentDefinition'],
['enter', 'SelectionSet', 'selectionSet', 'FragmentDefinition'],
['enter', 'Field', 0, null],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['enter', 'Argument', 0, null],
['enter', 'Name', 'name', 'Argument'],
['leave', 'Name', 'name', 'Argument'],
['enter', 'Variable', 'value', 'Argument'],
['enter', 'Name', 'name', 'Variable'],
['leave', 'Name', 'name', 'Variable'],
['leave', 'Variable', 'value', 'Argument'],
['leave', 'Argument', 0, null],
['enter', 'Argument', 1, null],
['enter', 'Name', 'name', 'Argument'],
['leave', 'Name', 'name', 'Argument'],
['enter', 'Variable', 'value', 'Argument'],
['enter', 'Name', 'name', 'Variable'],
['leave', 'Name', 'name', 'Variable'],
['leave', 'Variable', 'value', 'Argument'],
['leave', 'Argument', 1, null],
['enter', 'Argument', 2, null],
['enter', 'Name', 'name', 'Argument'],
['leave', 'Name', 'name', 'Argument'],
['enter', 'ObjectValue', 'value', 'Argument'],
['enter', 'ObjectField', 0, null],
['enter', 'Name', 'name', 'ObjectField'],
['leave', 'Name', 'name', 'ObjectField'],
['enter', 'StringValue', 'value', 'ObjectField'],
['leave', 'StringValue', 'value', 'ObjectField'],
['leave', 'ObjectField', 0, null],
['enter', 'ObjectField', 1, null],
['enter', 'Name', 'name', 'ObjectField'],
['leave', 'Name', 'name', 'ObjectField'],
['enter', 'StringValue', 'value', 'ObjectField'],
['leave', 'StringValue', 'value', 'ObjectField'],
['leave', 'ObjectField', 1, null],
['leave', 'ObjectValue', 'value', 'Argument'],
['leave', 'Argument', 2, null],
['leave', 'Field', 0, null],
['leave', 'SelectionSet', 'selectionSet', 'FragmentDefinition'],
['leave', 'FragmentDefinition', 3, null],
['enter', 'OperationDefinition', 4, null],
['enter', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
['enter', 'Field', 0, null],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['enter', 'Argument', 0, null],
['enter', 'Name', 'name', 'Argument'],
['leave', 'Name', 'name', 'Argument'],
['enter', 'BooleanValue', 'value', 'Argument'],
['leave', 'BooleanValue', 'value', 'Argument'],
['leave', 'Argument', 0, null],
['enter', 'Argument', 1, null],
['enter', 'Name', 'name', 'Argument'],
['leave', 'Name', 'name', 'Argument'],
['enter', 'BooleanValue', 'value', 'Argument'],
['leave', 'BooleanValue', 'value', 'Argument'],
['leave', 'Argument', 1, null],
['enter', 'Argument', 2, null],
['enter', 'Name', 'name', 'Argument'],
['leave', 'Name', 'name', 'Argument'],
['enter', 'NullValue', 'value', 'Argument'],
['leave', 'NullValue', 'value', 'Argument'],
['leave', 'Argument', 2, null],
['leave', 'Field', 0, null],
['enter', 'Field', 1, null],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['leave', 'Field', 1, null],
['leave', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
['leave', 'OperationDefinition', 4, null],
['leave', 'Document', null, null],
], $visited);
}
public function testVisitInParallel()
{
$visited = [];
/** @noinspection PhpUnhandledExceptionInspection */
$ast = parse('{ a, b { x }, c }');
$visitor = new ParallelVisitor([
new Visitor(
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = ['enter', $node->getKind(), $node instanceof NameNode ? $node->getValue() : null];
if ($node instanceof FieldNode && $node->getNameValue() === 'b') {
return new VisitorResult(null);
}
return new VisitorResult($node);
},
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = ['leave', $node->getKind(), $node instanceof NameNode ? $node->getValue() : null];
return new VisitorResult($node);
}
),
]);
try {
$ast->acceptVisitor(new VisitorInfo($visitor));
} catch (VisitorBreak $e) {
}
$this->assertEquals([
['enter', 'Document', null],
['enter', 'OperationDefinition', null],
['enter', 'SelectionSet', null],
['enter', 'Field', null],
['enter', 'Name', 'a'],
['leave', 'Name', 'a'],
['leave', 'Field', null],
['enter', 'Field', null],
['enter', 'Field', null],
['enter', 'Name', 'c'],
['leave', 'Name', 'c'],
['leave', 'Field', null],
['leave', 'SelectionSet', null],
['leave', 'OperationDefinition', null],
['leave', 'Document', null],
], $visited);
}
public function testAllowsSkippingSubTreeWhenVisitingInParallel()
{
$visited = [];
/** @noinspection PhpUnhandledExceptionInspection */
$ast = parse('{ a { x }, b { y } }', ['noLocation' => true]);
$visitor = new ParallelVisitor([
new Visitor(
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = [
'no-a',
'enter',
$node->getKind(),
$node instanceof NameNode ? $node->getValue() : null
];
if ($node instanceof FieldNode && $node->getNameValue() === 'a') {
return new VisitorResult(null);
}
return new VisitorResult($node);
},
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = [
'no-a',
'leave',
$node->getKind(),
$node instanceof NameNode ? $node->getValue() : null
];
return new VisitorResult($node);
}
),
new Visitor(
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = [
'no-b',
'enter',
$node->getKind(),
$node instanceof NameNode ? $node->getValue() : null
];
if ($node instanceof FieldNode && $node->getNameValue() === 'b') {
return new VisitorResult(null);
}
return new VisitorResult($node);
},
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = [
'no-b',
'leave',
$node->getKind(),
$node instanceof NameNode ? $node->getValue() : null
];
return new VisitorResult($node);
}
),
]);
try {
$ast->acceptVisitor(new VisitorInfo($visitor));
} catch (VisitorBreak $e) {
}
$this->assertEquals([
['no-a', 'enter', 'Document', null],
['no-b', 'enter', 'Document', null],
['no-a', 'enter', 'OperationDefinition', null],
['no-b', 'enter', 'OperationDefinition', null],
['no-a', 'enter', 'SelectionSet', null],
['no-b', 'enter', 'SelectionSet', null],
['no-a', 'enter', 'Field', null],
['no-b', 'enter', 'Field', null],
['no-b', 'enter', 'Name', 'a'],
['no-b', 'leave', 'Name', 'a'],
['no-b', 'enter', 'SelectionSet', null],
['no-b', 'enter', 'Field', null],
['no-b', 'enter', 'Name', 'x'],
['no-b', 'leave', 'Name', 'x'],
['no-b', 'leave', 'Field', null],
['no-b', 'leave', 'SelectionSet', null],
['no-b', 'leave', 'Field', null],
['no-a', 'enter', 'Field', null],
['no-b', 'enter', 'Field', null],
['no-a', 'enter', 'Name', 'b'],
['no-a', 'leave', 'Name', 'b'],
['no-a', 'enter', 'SelectionSet', null],
['no-a', 'enter', 'Field', null],
['no-a', 'enter', 'Name', 'y'],
['no-a', 'leave', 'Name', 'y'],
['no-a', 'leave', 'Field', null],
['no-a', 'leave', 'SelectionSet', null],
['no-a', 'leave', 'Field', null],
['no-a', 'leave', 'SelectionSet', null],
['no-b', 'leave', 'SelectionSet', null],
['no-a', 'leave', 'OperationDefinition', null],
['no-b', 'leave', 'OperationDefinition', null],
['no-a', 'leave', 'Document', null],
['no-b', 'leave', 'Document', null],
], $visited);
}
public function testAllowsEarlyExitWhileEnteringWhenVisitingInParallel()
{
$visited = [];
/** @noinspection PhpUnhandledExceptionInspection */
$ast = parse('{ a, b { x }, c }', ['noLocation' => true]);
$visitor = new ParallelVisitor([
new Visitor(
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = ['enter', $node->getKind(), $node instanceof NameNode ? $node->getValue() : null];
if ($node instanceof NameNode && $node->getValue() === 'x') {
return new VisitorResult($node, VisitorResult::ACTION_BREAK);
}
return new VisitorResult($node);
},
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = ['leave', $node->getKind(), $node instanceof NameNode ? $node->getValue() : null];
return new VisitorResult($node);
}
),
]);
try {
$ast->acceptVisitor(new VisitorInfo($visitor));
} catch (VisitorBreak $e) {
}
$this->assertEquals([
['enter', 'Document', null],
['enter', 'OperationDefinition', null],
['enter', 'SelectionSet', null],
['enter', 'Field', null],
['enter', 'Name', 'a'],
['leave', 'Name', 'a'],
['leave', 'Field', null],
['enter', 'Field', null],
['enter', 'Name', 'b'],
['leave', 'Name', 'b'],
['enter', 'SelectionSet', null],
['enter', 'Field', null],
['enter', 'Name', 'x'],
], $visited);
}
public function testAllowsEarlyExitWhileLeavingWhenVisitingInParallel()
{
$visited = [];
/** @noinspection PhpUnhandledExceptionInspection */
$ast = parse('{ a, b { x }, c }', ['noLocation' => true]);
$visitor = new ParallelVisitor([
new Visitor(
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = ['enter', $node->getKind(), $node instanceof NameNode ? $node->getValue() : null];
return new VisitorResult($node);
},
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = ['leave', $node->getKind(), $node instanceof NameNode ? $node->getValue() : null];
if ($node instanceof NameNode && $node->getValue() === 'x') {
return new VisitorResult($node, VisitorResult::ACTION_BREAK);
}
return new VisitorResult($node);
}
),
]);
try {
$ast->acceptVisitor(new VisitorInfo($visitor));
} catch (VisitorBreak $e) {
}
$this->assertEquals([
['enter', 'Document', null],
['enter', 'OperationDefinition', null],
['enter', 'SelectionSet', null],
['enter', 'Field', null],
['enter', 'Name', 'a'],
['leave', 'Name', 'a'],
['leave', 'Field', null],
['enter', 'Field', null],
['enter', 'Name', 'b'],
['leave', 'Name', 'b'],
['enter', 'SelectionSet', null],
['enter', 'Field', null],
['enter', 'Name', 'x'],
['leave', 'Name', 'x'],
], $visited);
}
public function testAllowsEarlyExitFromDifferentPointsWhenVisitingInParallel()
{
$visited = [];
/** @noinspection PhpUnhandledExceptionInspection */
$ast = parse('{ a { y }, b { x } }', ['noLocation' => true]);
$visitor = new ParallelVisitor([
new Visitor(
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = [
'break-a',
'enter',
$node->getKind(),
$node instanceof NameNode ? $node->getValue() : null
];
if ($node instanceof NameNode && $node->getValue() === 'a') {
return new VisitorResult($node, VisitorResult::ACTION_BREAK);
}
return new VisitorResult($node);
},
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = [
'break-a',
'leave',
$node->getKind(),
$node instanceof NameNode ? $node->getValue() : null
];
return new VisitorResult($node);
}
),
new Visitor(
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = [
'break-b',
'enter',
$node->getKind(),
$node instanceof NameNode ? $node->getValue() : null
];
if ($node instanceof NameNode && $node->getValue() === 'b') {
return new VisitorResult($node, VisitorResult::ACTION_BREAK);
}
return new VisitorResult($node);
},
function (NodeInterface $node) use (&$visited): VisitorResult {
$visited[] = [
'break-b',
'leave',
$node->getKind(),
$node instanceof NameNode ? $node->getValue() : null
];
return new VisitorResult($node);
}
),
]);
try {
$ast->acceptVisitor(new VisitorInfo($visitor));
} catch (VisitorBreak $e) {
}
$this->assertEquals([
['break-a', 'enter', 'Document', null],
['break-b', 'enter', 'Document', null],
['break-a', 'enter', 'OperationDefinition', null],
['break-b', 'enter', 'OperationDefinition', null],
['break-a', 'enter', 'SelectionSet', null],
['break-b', 'enter', 'SelectionSet', null],
['break-a', 'enter', 'Field', null],
['break-b', 'enter', 'Field', null],
['break-a', 'enter', 'Name', 'a'],
['break-b', 'enter', 'Name', 'a'],
['break-b', 'leave', 'Name', 'a'],
['break-b', 'enter', 'SelectionSet', null],
['break-b', 'enter', 'Field', null],
['break-b', 'enter', 'Name', 'y'],
['break-b', 'leave', 'Name', 'y'],
['break-b', 'leave', 'Field', null],
['break-b', 'leave', 'SelectionSet', null],
['break-b', 'leave', 'Field', null],
['break-b', 'enter', 'Field', null],
['break-b', 'enter', 'Name', 'b'],
], $visited);
}
public function testMaintainsTypeInfoDuringVisit()
{
$visited = [];
/** @noinspection PhpUnhandledExceptionInspection */
$ast = parse('{ human(id: 4) { name, pets { ... { name } }, unknown } }');
$typeInfo = new TypeInfo(testSchema());
$visitor = new TypeInfoVisitor(
$typeInfo,
new Visitor(
function (NodeInterface $node) use (&$visited, $typeInfo): VisitorResult {
$parentType = $typeInfo->getParentType();
$type = $typeInfo->getType();
$inputType = $typeInfo->getInputType();
$visited[] = [
'enter',
$node->getKind(),
$node instanceof NameNode ? $node->getValue() : null,
$parentType ? (string)$parentType : null,
$type ? (string)$type : null,
$inputType ? (string)$inputType : null,
];
return new VisitorResult($node);
},
function (NodeInterface $node) use (&$visited, $typeInfo): VisitorResult {
$parentType = $typeInfo->getParentType();
$type = $typeInfo->getType();
$inputType = $typeInfo->getInputType();
$visited[] = [
'leave',
$node->getKind(),
$node instanceof NameNode ? $node->getValue() : null,
$parentType ? (string)$parentType : null,
$type ? (string)$type : null,
$inputType ? (string)$inputType : null,
];
return new VisitorResult($node);
}
)
);
try {
$ast->acceptVisitor(new VisitorInfo($visitor));
} catch (VisitorBreak $e) {
}
$this->assertEquals([
['enter', 'Document', null, null, null, null],
['enter', 'OperationDefinition', null, null, 'QueryRoot', null],
['enter', 'SelectionSet', null, 'QueryRoot', 'QueryRoot', null],
['enter', 'Field', null, 'QueryRoot', 'Human', null],
['enter', 'Name', 'human', 'QueryRoot', 'Human', null],
['leave', 'Name', 'human', 'QueryRoot', 'Human', null],
['enter', 'Argument', null, 'QueryRoot', 'Human', 'ID'],
['enter', 'Name', 'id', 'QueryRoot', 'Human', 'ID'],
['leave', 'Name', 'id', 'QueryRoot', 'Human', 'ID'],
['enter', 'IntValue', null, 'QueryRoot', 'Human', 'ID'],
['leave', 'IntValue', null, 'QueryRoot', 'Human', 'ID'],
['leave', 'Argument', null, 'QueryRoot', 'Human', 'ID'],
['enter', 'SelectionSet', null, 'Human', 'Human', null],
['enter', 'Field', null, 'Human', 'String', null],
['enter', 'Name', 'name', 'Human', 'String', null],
['leave', 'Name', 'name', 'Human', 'String', null],
['leave', 'Field', null, 'Human', 'String', null],
['enter', 'Field', null, 'Human', '[Pet]', null],
['enter', 'Name', 'pets', 'Human', '[Pet]', null],
['leave', 'Name', 'pets', 'Human', '[Pet]', null],
['enter', 'SelectionSet', null, 'Pet', '[Pet]', null],
['enter', 'InlineFragment', null, 'Pet', 'Pet', null],
['enter', 'SelectionSet', null, 'Pet', 'Pet', null],
['enter', 'Field', null, 'Pet', 'String', null],
['enter', 'Name', 'name', 'Pet', 'String', null],
['leave', 'Name', 'name', 'Pet', 'String', null],
['leave', 'Field', null, 'Pet', 'String', null],
['leave', 'SelectionSet', null, 'Pet', 'Pet', null],
['leave', 'InlineFragment', null, 'Pet', 'Pet', null],
['leave', 'SelectionSet', null, 'Pet', '[Pet]', null],
['leave', 'Field', null, 'Human', '[Pet]', null],
['enter', 'Field', null, 'Human', null, null],
['enter', 'Name', 'unknown', 'Human', null, null],
['leave', 'Name', 'unknown', 'Human', null, null],
['leave', 'Field', null, 'Human', null, null],
['leave', 'SelectionSet', null, 'Human', 'Human', null],
['leave', 'Field', null, 'QueryRoot', 'Human', null],
['leave', 'SelectionSet', null, 'QueryRoot', 'QueryRoot', null],
['leave', 'OperationDefinition', null, null, 'QueryRoot', null],
['leave', 'Document', null, null, null, null],
], $visited);
}
public function testMaintainsTypeInfoDuringEdit()
{
$visited = [];
/** @noinspection PhpUnhandledExceptionInspection */
$ast = parse('{ human(id: 4) { name, pets }, alien }');
$nodeBuilder = GraphQL::make(NodeBuilderInterface::class);
$typeInfo = new TypeInfo(testSchema());
$visitor = new TypeInfoVisitor(
$typeInfo,
new Visitor(
function (NodeInterface $node) use (&$visited, $typeInfo, $nodeBuilder): VisitorResult {
$parentType = $typeInfo->getParentType();
$type = $typeInfo->getType();
$inputType = $typeInfo->getInputType();
$visited[] = [
'enter',
$node->getKind(),
$node instanceof NameNode ? $node->getValue() : null,
$parentType ? (string)$parentType : null,
$type ? (string)$type : null,
$inputType ? (string)$inputType : null,
];
if ($node instanceof FieldNode
&& null === $node->getSelectionSet()
&& getNamedType($type) instanceof CompositeTypeInterface
) {
return new VisitorResult($nodeBuilder->build([
'kind' => NodeKindEnum::FIELD,
'alias' => $node->getAliasAST(),
'name' => $node->getNameAST(),
'arguments' => $node->getArgumentsAST(),
'directives' => $node->getDirectivesAST(),
'selectionSet' => [
'kind' => NodeKindEnum::SELECTION_SET,
'fields' => [
[
'kind' => NodeKindEnum::FIELD,
'name' => ['value' => '__typename'],
]
]
],
]), VisitorResult::ACTION_REPLACE);
}
return new VisitorResult($node);
},
function (NodeInterface $node) use (&$visited, $typeInfo): VisitorResult {
$parentType = $typeInfo->getParentType();
$type = $typeInfo->getType();
$inputType = $typeInfo->getInputType();
$visited[] = [
'leave',
$node->getKind(),
$node instanceof NameNode ? $node->getValue() : null,
$parentType ? (string)$parentType : null,
$type ? (string)$type : null,
$inputType ? (string)$inputType : null,
];
return new VisitorResult($node);
}
)
);
try {
$ast->acceptVisitor(new VisitorInfo($visitor));
} catch (VisitorBreak $e) {
}
// TODO: Add asserts for print once the printer is implemented
$this->markTestIncomplete('BUG: Nodes added by visitors are visited either too early or too late.');
$this->assertEquals([
['enter', 'Document', null, null, null, null],
['enter', 'OperationDefinition', null, null, 'QueryRoot', null],
['enter', 'SelectionSet', null, 'QueryRoot', 'QueryRoot', null],
['enter', 'Field', null, 'QueryRoot', 'Human', null],
['enter', 'Name', 'human', 'QueryRoot', 'Human', null],
['leave', 'Name', 'human', 'QueryRoot', 'Human', null],
['enter', 'Argument', null, 'QueryRoot', 'Human', 'ID'],
['enter', 'Name', 'id', 'QueryRoot', 'Human', 'ID'],
['leave', 'Name', 'id', 'QueryRoot', 'Human', 'ID'],
['enter', 'IntValue', null, 'QueryRoot', 'Human', 'ID'],
['leave', 'IntValue', null, 'QueryRoot', 'Human', 'ID'],
['leave', 'Argument', null, 'QueryRoot', 'Human', 'ID'],
['enter', 'SelectionSet', null, 'Human', 'Human', null],
['enter', 'Field', null, 'Human', 'String', null],
['enter', 'Name', 'name', 'Human', 'String', null],
['leave', 'Name', 'name', 'Human', 'String', null],
['leave', 'Field', null, 'Human', 'String', null],
['enter', 'Field', null, 'Human', '[Pet]', null],
['enter', 'Name', 'pets', 'Human', '[Pet]', null],
['leave', 'Name', 'pets', 'Human', '[Pet]', null],
['enter', 'SelectionSet', null, 'Pet', '[Pet]', null],
['enter', 'Field', null, 'Pet', 'String!', null],
['enter', 'Name', '__typename', 'Pet', 'String!', null],
['leave', 'Name', '__typename', 'Pet', 'String!', null],
['leave', 'Field', null, 'Pet', 'String!', null],
['leave', 'SelectionSet', null, 'Pet', '[Pet]', null],
['leave', 'Field', null, 'Human', '[Pet]', null],
['leave', 'SelectionSet', null, 'Human', 'Human', null],
['leave', 'Field', null, 'QueryRoot', 'Human', null],
['enter', 'Field', null, 'QueryRoot', 'Alien', null],
['enter', 'Name', 'alien', 'QueryRoot', 'Alien', null],
['leave', 'Name', 'alien', 'QueryRoot', 'Alien', null],
['enter', 'SelectionSet', null, 'Alien', 'Alien', null],
['enter', 'Field', null, 'Alien', 'String!', null],
['enter', 'Name', '__typename', 'Alien', 'String!', null],
['leave', 'Name', '__typename', 'Alien', 'String!', null],
['leave', 'Field', null, 'Alien', 'String!', null],
['leave', 'SelectionSet', null, 'Alien', 'Alien', null],
['leave', 'Field', null, 'QueryRoot', 'Alien', null],
['leave', 'SelectionSet', null, 'QueryRoot', 'QueryRoot', null],
['leave', 'OperationDefinition', null, null, 'QueryRoot', null],
['leave', 'Document', null, null, null, null],
], $visited);
}
}
class AddedFieldNode extends FieldNode
{
}
================================================
FILE: tests/Functional/Language/kitchen-sink.graphql
================================================
# Copyright (c) 2015-present, Facebook, Inc.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
query queryName($foo: ComplexType, $site: Site = MOBILE) {
whoever123is: node(id: [123, 456]) {
id ,
... on User @defer {
field2 {
id ,
alias: field1(first:10, after:$foo,) @include(if: $foo) {
id,
...frag
}
}
}
... @skip(unless: $foo) {
id
}
... {
id
}
}
}
mutation likeStory {
like(story: 123) @defer {
story {
id
}
}
}
subscription StoryLikeSubscription($input: StoryLikeSubscribeInput) {
storyLikeSubscribe(input: $input) {
story {
likers {
count
}
likeSentence {
text
}
}
}
}
fragment frag on Friend {
foo(size: $size, bar: $b, obj: {key: "value", block: """
block string uses \"""
"""})
}
{
unnamed(truthy: true, falsey: false, nullish: null),
query
}
================================================
FILE: tests/Functional/Language/schema-kitchen-sink.graphqls
================================================
schema {
query: QueryType
mutation: MutationType
}
"""
This is a description
of the `Foo` type.
"""
type Foo implements Bar & Baz {
one: Type
two(argument: InputType!): Type
three(argument: InputType, other: String): Int
four(argument: String = "string"): String
five(argument: [String] = ["string", "string"]): String
six(argument: InputType = {key: "value"}): Type
seven(argument: Int = null): Type
}
type AnnotatedObject @onObject(arg: "value") {
annotatedField(arg: Type = "default" @onArg): Type @onField
}
type UndefinedType
extend type Foo {
seven(argument: [String]): Type
}
extend type Foo @onType
interface Bar {
one: Type
four(argument: String = "string"): String
}
interface AnnotatedInterface @onInterface {
annotatedField(arg: Type @onArg): Type @onField
}
interface UndefinedInterface
extend interface Bar {
two(argument: InputType!): Type
}
extend interface Bar @onInterface
union Feed = Story | Article | Advert
union AnnotatedUnion @onUnion = A | B
union AnnotatedUnionTwo @onUnion = | A | B
union UndefinedUnion
extend union Feed = Photo | Video
extend union Feed @onUnion
scalar CustomScalar
scalar AnnotatedScalar @onScalar
extend scalar CustomScalar @onScalar
enum Site {
DESKTOP
MOBILE
}
enum AnnotatedEnum @onEnum {
ANNOTATED_VALUE @onEnumValue
OTHER_VALUE
}
enum UndefinedEnum
extend enum Site {
VR
}
extend enum Site @onEnum
input InputType {
key: String!
answer: Int = 42
}
input AnnotatedInput @onInputObject {
annotatedField: Type @onField
}
input UndefinedInput
extend input InputType {
other: Float = 1.23e4
}
extend input InputType @onInputObject
directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
directive @include(if: Boolean!)
on FIELD
| FRAGMENT_SPREAD
| INLINE_FRAGMENT
directive @include2(if: Boolean!) on
| FIELD
| FRAGMENT_SPREAD
| INLINE_FRAGMENT
================================================
FILE: tests/Functional/QueryTest.php
================================================
assertEquals([
'data' => [
'hero' => [
'name' => 'R2-D2',
],
],
], $result);
}
// Skip: "Accepts an object with named properties to graphql()"
// Allows us to query for the ID and friends of R2-D2
public function testAllowsUsToQueryForTheIDAndFieldsOfR2D2()
{
$query = '
query HeroNameQuery {
hero {
id
name
friends {
name
}
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'hero' => [
'id' => '2001',
'name' => 'R2-D2',
'friends' => [
['name' => 'Luke Skywalker'],
['name' => 'Han Solo'],
['name' => 'Leia Organa'],
],
],
],
], $result);
}
// Nested Queries
// Allows us to query for the friends of friends of R2-D2
public function testAllowsUsToQueryForTheFriendsOfFriendsOfR2D2()
{
$query = '
query HeroNameQuery {
hero {
name
friends {
name
appearsIn
friends {
name
}
}
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'hero' => [
'name' => 'R2-D2',
'friends' => [
[
'name' => 'Luke Skywalker',
'appearsIn' => ['NEWHOPE', 'EMPIRE', 'JEDI'],
'friends' => [
['name' => 'Han Solo'],
['name' => 'Leia Organa'],
['name' => 'C-3PO'],
['name' => 'R2-D2'],
]
],
[
'name' => 'Han Solo',
'appearsIn' => ['NEWHOPE', 'EMPIRE', 'JEDI'],
'friends' => [
['name' => 'Luke Skywalker'],
['name' => 'Leia Organa'],
['name' => 'R2-D2'],
]
],
[
'name' => 'Leia Organa',
'appearsIn' => ['NEWHOPE', 'EMPIRE', 'JEDI'],
'friends' => [
['name' => 'Luke Skywalker'],
['name' => 'Han Solo'],
['name' => 'C-3PO'],
['name' => 'R2-D2'],
]
],
],
],
],
], $result);
}
// Using IDs and query parameters to refetch objects
// Allows us to query for Luke Skywalker directly, using his ID
public function testAllowsUsToQueryForLukeSkywalkerDirectlyUsingHisID()
{
$query = '
query FetchLukeQuery {
human(id: "1000") {
name
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'human' => [
'name' => 'Luke Skywalker',
],
],
], $result);
}
// Allows us to create a generic query, then use it to fetch Luke Skywalker using his ID
public function testAllowsUsToCreateAGenericQueryThenUseItToFetchLukeSkywalkerUsingHisID()
{
$query = '
query FetchSomeIDQuery($someId: String!) {
human(id: $someId) {
name
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query, null, null, ['someId' => '1000']);
$this->assertEquals([
'data' => [
'human' => [
'name' => 'Luke Skywalker',
],
],
], $result);
}
// Allows us to create a generic query, then use it to fetch Han Solo using his ID
public function testAllowsUsToCreateAGenericQueryThenUseItToFetchHanSoloUsingHisID()
{
$query = '
query FetchSomeIDQuery($someId: String!) {
human(id: $someId) {
name
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query, null, null, ['someId' => '1002']);
$this->assertEquals([
'data' => [
'human' => [
'name' => 'Han Solo',
],
],
], $result);
}
// Allows us to create a generic query, then pass an invalid ID to get null back
public function testAllowsUsToCreateAGenericQueryThenPassAnInvalidIdToGetBackNull()
{
$query = '
query FetchSomeIDQuery($someId: String!) {
human(id: $someId) {
name
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query, null, null, ['someId' => 'not a valid id']);
$this->assertEquals([
'data' => [
'human' => null,
],
], $result);
}
// Using aliases to change the key in the response
// Allows us to query for Luke, changing his key with an alias
public function testAllowsUsToQueryForLukeChangingHisKeyWithAnAlias()
{
$query = '
query FetchLukeAliased {
luke: human(id: "1000") {
name
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'luke' => [
'name' => 'Luke Skywalker',
],
],
], $result);
}
// Allows us to query for both Luke and Leia, using two root fields and an alias
public function testAllowsUsToQueryForBothLukeAndLeiaUsingTwoRootFieldsAndAnAlias()
{
$query = '
query FetchLukeAndLeiaAliased {
luke: human(id: "1000") {
name
}
leia: human(id: "1003") {
name
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'luke' => [
'name' => 'Luke Skywalker',
],
'leia' => [
'name' => 'Leia Organa',
],
],
], $result);
}
// Uses fragments to express more complex queries
// Allows us to query using duplicated content
public function testAllowsUsToQueryUsingDuplicatedContent()
{
$query = '
query FetchLukeAndLeiaAliased {
luke: human(id: "1000") {
name
homePlanet
}
leia: human(id: "1003") {
name
homePlanet
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'luke' => [
'name' => 'Luke Skywalker',
'homePlanet' => 'Tatooine',
],
'leia' => [
'name' => 'Leia Organa',
'homePlanet' => 'Alderaan',
],
],
], $result);
}
// Allows us to use a fragment to avoid duplicating content
public function testAllowsUstoUseAFragmentToAvoidDuplicatingContent()
{
$query = '
query FetchLukeAndLeiaAliased {
luke: human(id: "1000") {
...HumanFragment
}
leia: human(id: "1003") {
...HumanFragment
}
}
fragment HumanFragment on Human {
name
homePlanet
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'luke' => [
'name' => 'Luke Skywalker',
'homePlanet' => 'Tatooine',
],
'leia' => [
'name' => 'Leia Organa',
'homePlanet' => 'Alderaan',
],
],
], $result);
}
// Using __typename to find the type of an object
// Allows us to verify that R2-D2 is a droid
public function testAllowsUsToVerifyThatR2D2IsADroid()
{
$query = '
query CheckTypeOfR2 {
hero {
__typename
name
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'hero' => [
'__typename' => 'Droid',
'name' => 'R2-D2',
],
],
], $result);
}
// Allows us to verify that Luke is a human
public function testAllowsUsToVerifyThatLukeIsAHuman()
{
$query = '
query CheckTypeOfLuke {
hero(episode: EMPIRE) {
__typename
name
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'hero' => [
'__typename' => 'Human',
'name' => 'Luke Skywalker',
],
],
], $result);
}
// Reporting errors raised in resolvers
// Correctly reports error on accessing secretBackstory
public function testCorrectlyReportsErrorOnAccessingSecretBackstory()
{
$query = '
query HeroNameQuery {
hero {
name
secretBackstory
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'hero' => [
'name' => 'R2-D2',
'secretBackstory' => null,
],
],
'errors' => [
[
'message' => 'secretBackstory is secret.',
'locations' => [locationShorthandToArray([5, 13])],
'path' => ['hero', 'secretBackstory'],
]
],
], $result);
}
// Correctly reports error on accessing secretBackstory in a list
public function testCorrectlyReportsErrorOnAccessingSecretBackstoryInAList()
{
$query = '
query HeroNameQuery {
hero {
name
friends {
name
secretBackstory
}
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'hero' => [
'name' => 'R2-D2',
'friends' => [
[
'name' => 'Luke Skywalker',
'secretBackstory' => null,
],
[
'name' => 'Han Solo',
'secretBackstory' => null,
],
[
'name' => 'Leia Organa',
'secretBackstory' => null,
],
],
],
],
'errors' => [
[
'message' => 'secretBackstory is secret.',
'locations' => [locationShorthandToArray([7, 15])],
'path' => ['hero', 'friends', 0, 'secretBackstory'],
],
[
'message' => 'secretBackstory is secret.',
'locations' => [locationShorthandToArray([7, 15])],
'path' => ['hero', 'friends', 1, 'secretBackstory'],
],
[
'message' => 'secretBackstory is secret.',
'locations' => [locationShorthandToArray([7, 15])],
'path' => ['hero', 'friends', 2, 'secretBackstory'],
]
],
], $result);
}
// Correctly reports error on accessing through an alias
public function testCorrectlyReportsErrorOnAccessingThroughAnAlias()
{
$query = '
query HeroNameQuery {
mainHero: hero {
name
story: secretBackstory
}
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql(starWarsSchema(), $query);
$this->assertEquals([
'data' => [
'mainHero' => [
'name' => 'R2-D2',
'story' => null,
],
],
'errors' => [
[
'message' => 'secretBackstory is secret.',
'locations' => [locationShorthandToArray([5, 13])],
'path' => ['mainHero', 'story'],
]
],
], $result);
}
}
================================================
FILE: tests/Functional/Schema/BuildingTest.php
================================================
assertInstanceOf(Schema::class, $schema);
}
}
================================================
FILE: tests/Functional/Schema/DefinitionPrinterTest.php
================================================
assertPrintedSchemaEqualsBuiltSchema(dedent($source));
}
/**
* @return array
*/
public function printStringFieldsDataProvider(): array
{
return [
// // String field
// [
// '
// type Query {
// singleField: String
// }
// ',
// ],
// // [String] field
// [
// '
// type Query {
// singleField: [String]
// }
// ',
// ],
// // String! field
// [
// '
// type Query {
// singleField: String!
// }
// ',
// ],
// // [String]! field
// [
// '
// type Query {
// singleField: [String]!
// }
// ',
// ],
// // [String!] field
// [
// '
// type Query {
// singleField: [String!]
// }
// ',
// ],
// // [String!]! field
// [
// '
// type Query {
// singleField: [String!]!
// }
// ',
// ],
// // String field with Int argument
// [
// '
// type Query {
// singleField(argOne: Int): String
// }
// ',
// ],
// // String field with Int argument with default value
// [
// '
// type Query {
// singleField(argOne: Int = 2): String
// }
// ',
// ],
// String field with String argument with default value
// TODO: Make this data set work
// [
// '
// type Query {
// singleField(argOne: String = "tes\t de\fault"): String
// }
// ',
// ],
// String field with Int argument with default null
// TODO: Make this data set work, needs https://github.com/digiaonline/graphql-php/issues/239
// [
// '
// type Query {
// singleField(argOne: Int = null): String
// }
// ',
// ],
// String field with Int! argument
[
'
type Query {
singleField(argOne: Int!): String
}
',
],
// String field with multiple arguments
[
'
type Query {
singleField(argOne: Int, argTwo: String): String
}
',
],
// String field with multiple arguments, first is default
[
'
type Query {
singleField(argOne: Int = 1, argTwo: String, argThree: Boolean): String
}
',
],
// String field with multiple arguments, second is default
[
'
type Query {
singleField(argOne: Int, argTwo: String = "foo", argThree: Boolean): String
}
',
],
// String field with multiple arguments, last is default
[
'
type Query {
singleField(argOne: Int, argTwo: String, argThree: Boolean = false): String
}
',
],
];
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\PrintException
*/
public function testPrintObjectFields(): void
{
$source = dedent('
type Foo {
str: String
}
type Query {
foo: Foo
}
');
$this->assertPrintedSchemaEqualsBuiltSchema($source);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\PrintException
*/
public function testPrintCustomRootQuery(): void
{
$source = dedent('
schema {
query: CustomQueryType
}
type CustomQueryType {
bar: String
}
');
$this->assertPrintedSchemaEqualsBuiltSchema($source);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\PrintException
*/
public function testPrintInterfaces(): void
{
// Single interface
$source = dedent('
type Bar implements Foo {
str: String
}
interface Foo {
str: String
}
type Query {
bar: Bar
}
');
$this->assertPrintedSchemaEqualsBuiltSchema($source);
// Multiple interfaces
$source = dedent('
interface Baaz {
int: Int
}
type Bar implements Foo & Baaz {
str: String
int: Int
}
interface Foo {
str: String
}
type Query {
bar: Bar
}
');
$this->assertPrintedSchemaEqualsBuiltSchema($source);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\PrintException
*/
public function testPrintUnions(): void
{
$source = dedent('
type Bar {
str: String
}
type Foo {
bool: Boolean
}
union MultipleUnion = Foo | Bar
type Query {
single: SingleUnion
multiple: MultipleUnion
}
union SingleUnion = Foo
');
$this->assertPrintedSchemaEqualsBuiltSchema($source);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\PrintException
*/
public function testPrintInputType(): void
{
$source = dedent('
input InputType {
int: Int
}
type Query {
str(argOne: InputType): String
}
');
$this->assertPrintedSchemaEqualsBuiltSchema($source);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\PrintException
*/
public function testPrintCustomScalar(): void
{
$source = dedent('
scalar Odd
type Query {
odd: Odd
}
');
$this->assertPrintedSchemaEqualsBuiltSchema($source);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\PrintException
*/
public function testPrintEnum(): void
{
$source = dedent('
type Query {
rgb: RGB
}
enum RGB {
RED
GREEN
BLUE
}
');
$this->assertPrintedSchemaEqualsBuiltSchema($source);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\PrintException
*
* TODO: Add support for printing directives first
*/
public function testPrintCustomDirectives(): void
{
$source = dedent('
directive @customDirective on FIELD
type Query {
field: String
}
');
$this->assertPrintedSchemaEqualsBuiltSchema($source);
}
// TODO: Add tests for description printing, it's currently quite broken so no point in writing the tests yet
public function testPrintIntrospectionSchema(): void
{
$source = dedent('
"""
Directs the executor to include this field or fragment only when the \`if\` argument is true.
"""
directive @include(
"""Included when true."""
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
"""
Directs the executor to skip this field or fragment when the \`if\` argument is true.
"""
directive @skip(
"""Skipped when true."""
if: Boolean!
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
"""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 using the Markdown syntax
(as specified by [CommonMark](https://commonmark.org/).
"""
reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE
"""
A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.
In some cases, you need to provide options to alter GraphQL\'s execution behavior
in ways field arguments will not suffice, such as conditionally including or
skipping a field. Directives provide this by describing additional information
to the executor.
"""
type __Directive {
name: String!
description: String
locations: [__DirectiveLocation!]!
args: [__InputValue!]!
}
"""
A Directive can be adjacent to many parts of the GraphQL language, a
__DirectiveLocation describes one such possible adjacencies.
"""
enum __DirectiveLocation {
"""Location adjacent to a query operation."""
QUERY
"""Location adjacent to a mutation operation."""
MUTATION
"""Location adjacent to a subscription operation."""
SUBSCRIPTION
"""Location adjacent to a field."""
FIELD
"""Location adjacent to a fragment definition."""
FRAGMENT_DEFINITION
"""Location adjacent to a fragment spread."""
FRAGMENT_SPREAD
"""Location adjacent to an inline fragment."""
INLINE_FRAGMENT
"""Location adjacent to a variable definition."""
VARIABLE_DEFINITION
"""Location adjacent to a schema definition."""
SCHEMA
"""Location adjacent to a scalar definition."""
SCALAR
"""Location adjacent to an object type definition."""
OBJECT
"""Location adjacent to a field definition."""
FIELD_DEFINITION
"""Location adjacent to an argument definition."""
ARGUMENT_DEFINITION
"""Location adjacent to an interface definition."""
INTERFACE
"""Location adjacent to a union definition."""
UNION
"""Location adjacent to an enum definition."""
ENUM
"""Location adjacent to an enum value definition."""
ENUM_VALUE
"""Location adjacent to an input object type definition."""
INPUT_OBJECT
"""Location adjacent to an input object field definition."""
INPUT_FIELD_DEFINITION
}
"""
One possible value for a given Enum. Enum values are unique values, not a
placeholder for a string or numeric value. However an Enum value is returned in
a JSON response as a string.
"""
type __EnumValue {
name: String!
description: String
isDeprecated: Boolean!
deprecationReason: String
}
"""
Object and Interface types are described by a list of Fields, each of which has
a name, potentially a list of arguments, and a return type.
"""
type __Field {
name: String!
description: String
args: [__InputValue!]!
type: __Type!
isDeprecated: Boolean!
deprecationReason: String
}
"""
Arguments provided to Fields or Directives and the input fields of an
InputObject are represented as Input Values which describe their type and
optionally a default value.
"""
type __InputValue {
name: String!
description: String
type: __Type!
"""
A GraphQL-formatted string representing the default value for this input value.
"""
defaultValue: String
}
"""
A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all
available types and directives on the server, as well as the entry points for
query, mutation, and subscription operations.
"""
type __Schema {
"""A list of all types supported by this server."""
types: [__Type!]!
"""The type that query operations will be rooted at."""
queryType: __Type!
"""
If this server supports mutation, the type that mutation operations will be rooted at.
"""
mutationType: __Type
"""
If this server support subscription, the type that subscription operations will be rooted at.
"""
subscriptionType: __Type
"""A list of all directives supported by this server."""
directives: [__Directive!]!
}
"""
The fundamental unit of any GraphQL Schema is the type. There are many kinds of
types in GraphQL as represented by the \`__TypeKind\` enum.
Depending on the kind of a type, certain fields describe information about that
type. Scalar types provide no information beyond a name and description, while
Enum types provide their values. Object and Interface types provide the fields
they describe. Abstract types, Union and Interface, provide the Object types
possible at runtime. List and NonNull types compose other types.
"""
type __Type {
kind: __TypeKind!
name: String
description: String
fields(includeDeprecated: Boolean = false): [__Field!]
interfaces: [__Type!]
possibleTypes: [__Type!]
enumValues(includeDeprecated: Boolean = false): [__EnumValue!]
inputFields: [__InputValue!]
ofType: __Type
}
"""An enum describing what kind of type a given \`__Type\` is."""
enum __TypeKind {
"""Indicates this type is a scalar."""
SCALAR
"""
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.
"""
INTERFACE
"""Indicates this type is a union. \`possibleTypes\` is a valid field."""
UNION
"""Indicates this type is an enum. \`enumValues\` is a valid field."""
ENUM
"""
Indicates this type is an input object. \`inputFields\` is a valid field.
"""
INPUT_OBJECT
"""Indicates this type is a list. \`ofType\` is a valid field."""
LIST
"""Indicates this type is a non-null. \`ofType\` is a valid field."""
NON_NULL
}
');
$schema = buildSchema($source);
$printer = new DefinitionPrinter();
$this->assertEquals($source, $printer->printIntrospectionSchema($schema));
}
/**
* @param string $source
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\PrintException
*/
private function assertPrintedSchemaEqualsBuiltSchema(string $source): void
{
$schema = buildSchema($source);
$printer = new DefinitionPrinter();
$this->assertSame($source, $printer->printSchema($schema));
}
}
================================================
FILE: tests/Functional/Schema/ExtensionTest.php
================================================
extender = GraphQL::make(SchemaExtenderInterface::class);
/** @noinspection PhpUnhandledExceptionInspection */
$this->someInterfaceType = newInterfaceType([
'name' => 'SomeInterface',
'fields' => function () {
return [
'name' => ['type' => stringType()],
'some' => ['type' => $this->someInterfaceType],
];
},
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->fooType = newObjectType([
'name' => 'Foo',
'interfaces' => [$this->someInterfaceType],
'fields' => function () {
return [
'name' => ['type' => stringType()],
'some' => ['type' => $this->someInterfaceType],
'tree' => ['type' => newNonNull(newList($this->fooType))],
];
},
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->barType = newObjectType([
'name' => 'Bar',
'interfaces' => [$this->someInterfaceType],
'fields' => function () {
return [
'name' => ['type' => stringType()],
'some' => ['type' => $this->someInterfaceType],
'foo' => ['type' => $this->fooType],
];
},
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->bizType = newObjectType([
'name' => 'Biz',
'fields' => function () {
return [
'fizz' => ['type' => stringType()]
];
},
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->someUnionType = newUnionType([
'name' => 'SomeUnion',
'types' => [$this->fooType, $this->bizType],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->someEnumType = newEnumType([
'name' => 'SomeEnum',
'values' => [
'ONE' => ['value' => 1],
'TWO' => ['value' => 2],
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->testSchema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => function () {
return [
'foo' => ['type' => $this->fooType],
'someUnion' => ['type' => $this->someUnionType],
'someEnum' => ['type' => $this->someEnumType],
'someInterface' => [
'type' => $this->someInterfaceType,
'args' => ['id' => ['type' => newNonNull(idType())]],
],
];
},
]),
'types' => [$this->fooType, $this->barType],
]);
}
// returns the original schema when there are no type definitions
public function testReturnsTheOriginalSchemaWhenThereAreNoTypeDefinitions()
{
$extendedSchema = $this->extendTestSchema('{ field }');
$this->assertEquals($this->testSchema, $extendedSchema);
}
// extends without altering original schema
public function testExtendsWithoutAlteringOriginalSchema()
{
$extendedSchema = $this->extendTestSchema(dedent('
extend type Query {
newField: String
}
'));
$this->assertNotEquals($this->testSchema, $extendedSchema);
// TODO: Assert printed schemas
}
// can be used for limited execution
public function testCanBeUsedForLimitedExecution()
{
$extendedSchema = $this->extendTestSchema(dedent('
extend type Query {
newField: String
}
'));
/** @noinspection PhpUnhandledExceptionInspection */
$result = graphql($extendedSchema, '{ newField }', ['newField' => 123]);
$this->assertEquals(['newField' => '123'], $result['data']);
}
// can describe the extended fields
public function testCanDescribeTheExtendedFields()
{
$extendedSchema = $this->extendTestSchema(dedent('
extend type Query {
"New field description."
newField: String
}
'));
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals(
'New field description.',
$extendedSchema->getQueryType()->getField('newField')->getDescription()
);
}
// Skip: can describe the extended fields with legacy comments
// TODO: describes extended fields with strings when present
// correctly assign AST nodes to new and extended types
public function testCorrectlyAssignASTNodesToNewAndExtendedTypes()
{
$extendedSchema = $this->extendTestSchema(dedent('
extend type Query {
newField(testArg: TestInput): TestEnum
}
enum TestEnum {
TEST_VALUE
}
input TestInput {
testInputField: TestEnum
}
'));
/** @noinspection PhpUnhandledExceptionInspection */
$secondExtensionAST = parse(dedent('
extend type Query {
oneMoreNewField: TestUnion
}
union TestUnion = TestType
interface TestInterface {
interfaceField: String
}
type TestType implements TestInterface {
interfaceField: String
}
directive @test(arg: Int) on FIELD
'));
$extendedTwiceSchema = $this->extendSchema(
$extendedSchema,
$secondExtensionAST
);
$query = $extendedTwiceSchema->getQueryType();
$testInput = $extendedTwiceSchema->getType('TestInput');
$testEnum = $extendedTwiceSchema->getType('TestEnum');
$testUnion = $extendedTwiceSchema->getType('TestUnion');
$testInterface = $extendedTwiceSchema->getType('TestInterface');
$testType = $extendedTwiceSchema->getType('TestType');
$testDirective = $extendedTwiceSchema->getDirective('test');
$this->assertCount(2, $query->getExtensionAstNodes());
$this->assertCount(0, $testType->getExtensionAstNodes());
// TODO: Assert printed types
}
// builds types with deprecated fields/values
public function gestBuildTypesWithDeprecatedFieldsOrValues()
{
$extendedSchema = $this->extendTestSchema(dedent('
type TypeWithDeprecatedField {
newDeprecatedField: String @deprecated(reason: "not used anymore")
}
enum EnumWithDeprecatedValue {
DEPRECATED @deprecated(reason: "do not use")
}
'));
/** @var Field $deprecatedFieldDefinition */
/** @noinspection PhpUndefinedMethodInspection */
$deprecatedFieldDefinition = $extendedSchema
->getType('TypeWithDeprecatedField')
->getField('newDeprecatedField');
$this->assertTrue($deprecatedFieldDefinition->isDeprecated());
$this->assertEquals('not used anymore', $deprecatedFieldDefinition->getDeprecationReason());
/** @var EnumValue $deprecatedEnumDefinition */
/** @noinspection PhpUndefinedMethodInspection */
$deprecatedEnumDefinition = $extendedSchema
->getType('EnumWithDeprecatedValue')
->getValue('DEPRECATED');
$this->assertTrue($deprecatedEnumDefinition->isDeprecated());
$this->assertEquals('do not use', $deprecatedEnumDefinition->getDeprecationReason());
}
// extends objects with deprecated fields
public function testExtendsObjectsWithDeprecatedFields()
{
$extendedSchema = $this->extendTestSchema(dedent('
extend type Foo {
deprecatedField: String @deprecated(reason: "not used anymore")
}
'));
/** @var Field $deprecatedFieldDefinition */
/** @noinspection PhpUndefinedMethodInspection */
$deprecatedFieldDefinition = $extendedSchema
->getType('Foo')
->getField('deprecatedField');
$this->assertTrue($deprecatedFieldDefinition->isDeprecated());
$this->assertEquals('not used anymore', $deprecatedFieldDefinition->getDeprecationReason());
}
// extends objects by adding new unused types
public function testExtendsObjectsByAddingNewUnusedTypes()
{
$extendedSchema = $this->extendTestSchema(dedent('
type Unused {
someField: String
}
'));
$this->assertNotEquals($this->testSchema, $extendedSchema);
// TODO: Assert printed schemas
}
// TODO: extends objects by adding new fields with arguments
// TODO: extends objects by adding new fields with existing types
// TODO: extends objects by adding implemented interfaces
// TODO: extends objects by including new types
// TODO: extends objects by adding implemented new interfaces
// TODO: extends objects multiple times
// TODO: extends interfaces by adding new fields
// allows extension of interface with missing Object fields
public function testAllowsExtensionOfInterfaceWithMissingObjectFields()
{
$extendedSchema = $this->extendTestSchema(dedent('
extend interface SomeInterface {
newField: String
}
'));
$errors = validateSchema($extendedSchema);
$this->assertNotEmpty($errors);
// TODO: Assert printed schemas
}
// TODO: extends interfaces multiple times
// may extend mutations and subscriptions
public function testMayExtendMutationsAndSubscriptions()
{
/** @noinspection PhpUnhandledExceptionInspection */
$mutationSchema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => function () {
return [
'queryField' => ['type' => stringType()],
];
},
]),
'mutation' => newObjectType([
'name' => 'Mutation',
'fields' => function () {
return [
'mutationField' => ['type' => stringType()],
];
},
]),
'subscription' => newObjectType([
'name' => 'Subscription',
'fields' => function () {
return [
'subscriptionField' => ['type' => stringType()],
];
},
]),
]);
/** @noinspection PhpUnhandledExceptionInspection */
$document = parse(dedent('
extend type Query {
newQueryField: Int
}
extend type Mutation {
newMutationField: Int
}
extend type Subscription {
newSubscriptionField: Int
}
'));
$extendedSchema = $this->extendSchema($mutationSchema, $document);
$this->assertNotEquals($mutationSchema, $extendedSchema);
// TODO: Assert printed schemas
}
// may extend directives with new simple directive
public function testMayExtendDirectivesWithNewSimpleDirective()
{
$extendedSchema = $this->extendTestSchema(dedent('
directive @neat on QUERY
'));
$extendedDirective = $extendedSchema->getDirective('neat');
$this->assertEquals('neat', $extendedDirective->getName());
$this->assertContains('QUERY', $extendedDirective->getLocations());
}
// sets correct description when extending with a new directive
public function testSetsCorrectDescriptionWhenExtendingWithANewDirective()
{
$extendedSchema = $this->extendTestSchema(dedent('
"""
new directive
"""
directive @new on QUERY
'));
$extendedDirective = $extendedSchema->getDirective('new');
$this->assertEquals('new directive', $extendedDirective->getDescription());
}
// Skip: sets correct description using legacy comments
// may extend directives with new complex directive
public function testMayExtendDirectivesWithNewComplexDirective()
{
$extendedSchema = $this->extendTestSchema(dedent('
directive @profile(enable: Boolean! tag: String) on QUERY | FIELD
'));
$extendedDirective = $extendedSchema->getDirective('profile');
$this->assertContains('QUERY', $extendedDirective->getLocations());
$this->assertContains('FIELD', $extendedDirective->getLocations());
/** @noinspection PhpUnhandledExceptionInspection */
$arguments = $extendedDirective->getArguments();
$argument0 = $arguments[0];
$argument1 = $arguments[1];
$this->assertCount(2, $arguments);
$this->assertEquals('enable', $argument0->getName());
$this->assertInstanceOf(NonNullType::class, $argument0->getType());
$this->assertInstanceOf(ScalarType::class, $argument0->getType()->getOfType());
$this->assertEquals('tag', $argument1->getName());
$this->assertInstanceOf(ScalarType::class, $argument1->getType());
}
// does not allow replacing a default directive
public function testDoesNotAllowReplacingADefaultDirective()
{
$this->expectException(SchemaExtensionException::class);
$this->expectExceptionMessage('Directive "include" already exists in the schema. It cannot be redefined.');
$this->extendTestSchema('directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD');
}
// does not allow replacing a custom directive
public function testDoesNotAllowReplacingACustomDirective()
{
$extendedSchema = $this->extendTestSchema('directive @meow(if: Boolean!) on FIELD | FRAGMENT_SPREAD');
/** @noinspection PhpUnhandledExceptionInspection */
$sdl = parse('directive @meow(if: Boolean!) on FIELD | QUERY');
$this->expectException(SchemaExtensionException::class);
$this->expectExceptionMessage('Directive "meow" already exists in the schema. It cannot be redefined.');
$this->extendSchema($extendedSchema, $sdl);
}
// does not allow replacing an existing type
public function testDoesNotAllowReplacingAnExistingType()
{
$this->expectException(SchemaExtensionException::class);
$this->expectExceptionMessage(
'Field "Bar.foo" already exists in the schema. It cannot also be ' .
'defined in this type extension.'
);
$this->extendTestSchema(dedent('
extend type Bar {
foo: Foo
}
'));
}
// does not allow referencing an unknown type
public function testDoesNotAllowReferencingAnUnknownType()
{
$this->expectException(SchemaExtensionException::class);
$this->expectExceptionMessage(
'Unknown type: "Quix". Ensure that this type exists either in the ' .
'original schema, or is added in a type definition.'
);
$this->extendTestSchema(dedent('
extend type Bar {
quix: Quix
}
'));
}
// does not allow extending an unknown type
public function testDoesNotAllowExtendingAnUnknownType()
{
$this->expectException(SchemaExtensionException::class);
$this->expectExceptionMessage(
'Cannot extend type "UnknownType" because it does not exist in the existing schema.'
);
$this->extendTestSchema(dedent('
extend type UnknownType {
baz: String
}
'));
}
// does not allow extending an unknown interface type
public function testDoesNotAllowExtendingAnUnknownInterfaceType()
{
$this->expectException(SchemaExtensionException::class);
$this->expectExceptionMessage(
'Cannot extend type "UnknownInterfaceType" because it does not exist in the existing schema.'
);
$this->extendTestSchema(dedent('
extend interface UnknownInterfaceType {
baz: String
}
'));
}
// Skip: maintains configuration of the original schema object
// Skip: adds to the configuration of the original schema object
// does not allow extending a non-object type
// not an object
public function testDoesNotAllowExtendingANonObjectType()
{
$this->expectException(SchemaExtensionException::class);
$this->expectExceptionMessage('Cannot extend non-object type "SomeInterface".');
$this->extendTestSchema(dedent('
extend type SomeInterface {
baz: String
}
'));
}
// not an interface
public function testDoesNotAllowExtendingANonObjectInterfaceType()
{
$this->expectException(SchemaExtensionException::class);
$this->expectExceptionMessage('Cannot extend non-interface type "Foo".');
$this->extendTestSchema(dedent('
extend interface Foo {
baz: String
}
'));
}
// not a scalar
public function testDoesNotAllowExtendingANonObjectScalarType()
{
$this->expectException(SchemaExtensionException::class);
$this->expectExceptionMessage('Cannot extend non-object type "String".');
$this->extendTestSchema(dedent('
extend type String {
baz: String
}
'));
}
// can add additional root operation types
// does not automatically include common root type names
public function testDoesNotAutomaticallyIncludeCommonRootTypeNames()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
type Mutation {
doSomething: String
}
');
$schema = $this->extendSchema($this->testSchema, $node);
$this->assertNull($schema->getMutationType());
}
// does not allow new schema within an extension
public function testDoesNotAllowNewSchemaWithinAnExtension()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
schema {
mutation: Mutation
}
type Mutation {
doSomething: String
}
');
$this->expectException(SchemaExtensionException::class);
$this->expectExceptionMessage('Cannot define a new schema within a schema extension.');
$this->extendSchema($this->testSchema, $node);
}
// adds new root types via schema extension
public function testAddsNewRootTypesViaSchemaExtension()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
extend schema {
mutation: Mutation
}
type Mutation {
doSomething: String
}
');
$schema = $this->extendSchema($this->testSchema, $node);
$this->assertEquals('Mutation', $schema->getMutationType()->getName());
}
// adds multiple new root types via schema extension
public function testAddsMultipleNewRootTypesViaSchemaExtension()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
extend schema {
mutation: Mutation
subscription: Subscription
}
type Mutation {
doSomething: String
}
type Subscription {
hearSomething: String
}
');
$schema = $this->extendSchema($this->testSchema, $node);
$this->assertEquals('Mutation', $schema->getMutationType()->getName());
$this->assertEquals('Subscription', $schema->getSubscriptionType()->getName());
}
// applies multiple schema extensions
public function testAppliesMultipleSchemaExtension()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
extend schema {
mutation: Mutation
}
extend schema {
subscription: Subscription
}
type Mutation {
doSomething: String
}
type Subscription {
hearSomething: String
}
');
$schema = $this->extendSchema($this->testSchema, $node);
$this->assertEquals('Mutation', $schema->getMutationType()->getName());
$this->assertEquals('Subscription', $schema->getSubscriptionType()->getName());
}
// TODO: schema extension AST are available from schema object
// does not allow redefining an existing root type
public function testDoesNotAllowRedefiningAnExistingRootType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
extend schema {
query: SomeType
}
type SomeType {
seeSomething: String
}
');
$this->expectException(SchemaExtensionException::class);
$this->expectExceptionMessage('Must provide only one query type in schema.');
$this->extendSchema($this->testSchema, $node);
}
// does not allow defining a root operation type twice
public function testDoesNotAllowDefiningARootTypeTwice()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
extend schema {
mutation: Mutation
}
extend schema {
mutation: Mutation
}
type Mutation {
doSomething: String
}
');
$this->expectException(SchemaExtensionException::class);
$this->expectExceptionMessage('Must provide only one mutation type in schema.');
$this->extendSchema($this->testSchema, $node);
}
// does not allow defining a root operation type with different types
public function testDoesNotAllowDefiningARootTypeWithDifferentTypes()
{
/** @noinspection PhpUnhandledExceptionInspection */
$node = parse('
extend schema {
mutation: Mutation
}
extend schema {
mutation: SomethingElse
}
type Mutation {
doSomething: String
}
type SomethingElse {
doSomethingElse: String
}
');
$this->expectException(SchemaExtensionException::class);
$this->expectExceptionMessage('Must provide only one mutation type in schema.');
$this->extendSchema($this->testSchema, $node);
}
protected function extendSchema($schema, $document)
{
return $this->extender->extend($schema, $document);
}
protected function extendTestSchema($sdl)
{
/** @noinspection PhpUnhandledExceptionInspection */
$document = parse($sdl);
$extendedSchema = $this->extendSchema($this->testSchema, $document);
// TODO: Assert printed schemas
return $extendedSchema;
}
}
================================================
FILE: tests/Functional/Schema/SchemaBuilderTest.php
================================================
[
[
'name' => 'Han Solo',
'totalCredits' => 10,
'__typename' => 'Human',
],
[
'name' => 'R2-D2',
'primaryFunction' => 'Astromech',
'__typename' => 'Droid',
],
],
];
$result = graphql($schema, $query, $rootValue);
$this->assertEquals([
'data' => [
'characters' => [
[
'name' => 'Han Solo',
'totalCredits' => 10,
],
[
'name' => 'R2-D2',
'primaryFunction' => 'Astromech',
],
],
],
], $result);
}
/**
* @throws \Digia\GraphQL\Error\InvariantException
*/
public function testSpecifyingUnionTypeUsingTypeNameMetaFieldDefinition()
{
$source = '
type Query {
fruits: [Fruit]
}
union Fruit = Apple | Banana
type Apple {
color: String
}
type Banana {
length: Int
}
';
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema($source);
$query = '
{
fruits {
... on Apple {
color
}
... on Banana {
length
}
}
}
';
$rootValue = [
'fruits' => [
[
'color' => 'green',
'__typename' => 'Apple',
],
[
'length' => 5,
'__typename' => 'Banana',
],
],
];
$result = graphql($schema, $query, $rootValue);
$this->assertEquals([
'data' => [
'fruits' => [
[
'color' => 'green',
],
[
'length' => 5,
],
],
],
], $result);
}
}
================================================
FILE: tests/Functional/Schema/SchemaTest.php
================================================
interfaceType = newInterfaceType([
'name' => 'Interface',
'fields' => [
'fieldName' => [
'type' => stringType(),
],
],
]);
$this->implementingType = newObjectType([
'name' => 'Object',
'interfaces' => [$this->interfaceType],
'fields' => [
'fieldName' => [
'type' => stringType(),
'resolve' => function () {
return '';
},
]
],
]);
$this->directiveInputType = newInputObjectType([
'name' => 'DirInput',
'fields' => [
'field' => [
'type' => stringType(),
]
],
]);
$this->wrappedDirectiveInputType = newInputObjectType([
'name' => 'WrappedDirInput',
'fields' => [
'field' => [
'type' => stringType(),
],
],
]);
$this->directive = newDirective([
'name' => 'dir',
'locations' => ['OBJECT'],
'args' => [
'arg' => [
'type' => $this->directiveInputType,
],
'argList' => [
'type' => newList($this->wrappedDirectiveInputType),
],
],
'fields' => [
'field' => [
'type' => stringType(),
],
],
]);
$this->schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'getObject' => [
'type' => $this->interfaceType,
'resolve' => function () {
return '';
}
],
],
]),
'directives' => [
$this->directive,
],
]);
$this->schema;
}
/**
* @expectedException \Exception
*/
public function testThrowsHumanReadableErrorIfSchemaTypesIsNotDefined()
{
$this->schema->isPossibleType($this->interfaceType, $this->implementingType);
}
public function testIncludesInputTypesOnlyUsedInDirectives()
{
$typeMap = $this->schema->getTypeMap();
$this->assertArrayHasKey('DirInput', $typeMap);
$this->assertArrayHasKey('WrappedDirInput', $typeMap);
}
}
================================================
FILE: tests/Functional/Schema/ValidationTest.php
================================================
schemaValidator = GraphQL::make(SchemaValidatorInterface::class);
/** @noinspection PhpUnhandledExceptionInspection */
$this->someScalarType = newScalarType([
'name' => 'SomeScalar',
'serialize' => function () {
},
'parseValue' => function () {
},
'parseLiteral' => function () {
},
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->someObjectType = newObjectType([
'name' => 'SomeObject',
'fields' => ['f' => ['type' => stringType()]],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->someUnionType = newUnionType([
'name' => 'SomeUnion',
'types' => [$this->someObjectType],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->someInterfaceType = newInterfaceType([
'name' => 'SomeInterface',
'fields' => ['f' => ['type' => stringType()]],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->someEnumType = newEnumType([
'name' => 'SomeEnum',
'values' => ['ONLY' => []],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->someInputObjectType = newInputObjectType([
'name' => 'SomeInputObject',
'fields' => [
'val' => ['type' => stringType(), 'defaultValue' => 'hello'],
],
]);
$this->outputTypes = $this->withModifiers([
stringType(),
$this->someScalarType,
$this->someEnumType,
$this->someObjectType,
$this->someUnionType,
$this->someInterfaceType,
]);
$this->noOutputTypes = $this->withModifiers([$this->someInputObjectType]);
$this->inputTypes = $this->withModifiers([
stringType(),
$this->someScalarType,
$this->someEnumType,
$this->someInputObjectType,
]);
$this->noInputTypes = $this->withModifiers([
$this->someObjectType,
$this->someUnionType,
$this->someInterfaceType,
]);
}
// Type System: A Schema must have Object root types
// accepts a Schema whose query type is an object type
public function testAcceptsASchemaWhoseQueryTypeIsAnObjectType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: String
}
'));
$this->expectValid($schema);
/** @noinspection PhpUnhandledExceptionInspection */
$schemaWithDef = buildSchema(dedent('
schema {
query: QueryRoot
}
type QueryRoot {
test: String
}
'));
$this->expectValid($schemaWithDef);
}
// accepts a Schema whose query and mutation types are object types
public function testAcceptsASchemaWhoseQueryAndMutationTypesAreObjectTypes()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: String
}
type Mutation {
test: String
}
'));
$this->expectValid($schema);
/** @noinspection PhpUnhandledExceptionInspection */
$schemaWithDef = buildSchema(dedent('
schema {
query: QueryRoot
}
type QueryRoot {
test: String
}
type MutationRoot {
test: String
}
'));
$this->expectValid($schemaWithDef);
}
// accepts a Schema whose query and subscription types are object types
public function testAcceptsASchemaWhoseQueryAndSubscriptionTypesAreObjectTypes()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: String
}
type Subscription {
test: String
}
'));
$this->expectValid($schema);
/** @noinspection PhpUnhandledExceptionInspection */
$schemaWithDef = buildSchema(dedent('
schema {
query: QueryRoot
}
type QueryRoot {
test: String
}
type SubscriptionRoot {
test: String
}
'));
$this->expectValid($schemaWithDef);
}
// rejects a Schema without a query type
public function testRejectsASchemaWithoutAQueryType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Mutation {
test: String
}
'));
$this->expectInvalid($schema, [
[
'message' => 'Query root type must be provided.',
'locations' => null,
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$schemaWithDef = buildSchema(dedent('
schema {
mutation: MutationRoot
}
type MutationRoot {
test: String
}
'));
$this->expectInvalid($schemaWithDef, [
[
'message' => 'Query root type must be provided.',
'locations' => [locationShorthandToArray([1, 1])],
]
]);
}
// Skip: rejects a Schema whose query root type is not an Object type
// Skip: rejects a Schema whose mutation type is an input type
// Skip: rejects a Schema whose subscription type is an input type
// rejects a Schema whose directives are incorrectly typed
// public function testRejectsASchemaWhoseDirectivesAreIncorrectlyTypes()
// {
// /** @noinspection PhpUnhandledExceptionInspection */
// $schema = newSchema([
// 'query' => $this->someObjectType,
// 'directives' => ['somedirective']
// ]);
//
// $this->expectInvalid($schema, [
// [
// 'message' => 'Expected directive but got: somedirective.',
// ]
// ]);
// }
// Type System: Objects must have fields
// accepts an Object type with fields object
public function testAcceptsAnObjectTypeWithFieldsObject()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
field: SomeObject
}
type SomeObject {
field: String
}
'));
$this->expectValid($schema);
}
// rejects an Object type with missing fields
public function testRejectsAnObjectTypeWithMissingFields()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: IncompleteObject
}
type IncompleteObject
'));
$this->expectInvalid($schema, [
[
'message' => 'Type IncompleteObject must define one or more fields.',
'locations' => [],
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$manualSchema = $this->schemaWithFieldType(
newObjectType([
'name' => 'IncompleteObject',
'fields' => [],
])
);
$this->expectInvalid($manualSchema, [
[
'message' => 'Type IncompleteObject must define one or more fields.',
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$manualSchema2 = $this->schemaWithFieldType(
newObjectType([
'name' => 'IncompleteObject',
'fields' => function () {
return [];
},
])
);
$this->expectInvalid($manualSchema2, [
[
'message' => 'Type IncompleteObject must define one or more fields.',
]
]);
}
// rejects an Object type with incorrectly named fields
public function testRejectsAnObjectTypeWithIncorrectlyNamedFields()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = $this->schemaWithFieldType(
newObjectType([
'name' => 'SomeObject',
'fields' => [
'bad-name-with-dashes' => ['type' => stringType()],
],
])
);
$this->expectInvalid($schema, [
[
'message' => 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "bad-name-with-dashes" does not.',
]
]);
}
// Skip: accepts an Object type with explicitly allowed legacy named fields
// Skip: throws with bad value for explicitly allowed legacy names
// Type System: Fields args must be properly named
// accepts field args with valid names
public function testAcceptsFieldArgumentsWithValidNames()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = $this->schemaWithFieldType(
newObjectType([
'name' => 'SomeObject',
'fields' => [
'goodField' => [
'type' => stringType(),
'args' => [
'goodArg' => ['type' => stringType()],
],
],
],
])
);
$this->expectValid($schema);
}
// rejects field arg with invalid names
public function testRejectsFieldArgumentsWithInvalidNames()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = $this->schemaWithFieldType(
newObjectType([
'name' => 'SomeObject',
'fields' => [
'badField' => [
'type' => stringType(),
'args' => [
'bad-name-with-dashes' => ['type' => stringType()],
],
],
],
])
);
$this->expectInvalid($schema, [
[
'message' => 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "bad-name-with-dashes" does not.',
]
]);
}
// Type System: Union types must be valid
// accepts a Union type with member types
public function testAcceptsAUnionTypeWithMemberTypes()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: GoodUnion
}
type TypeA {
field: String
}
type TypeB {
field: String
}
union GoodUnion =
| TypeA
| TypeB
'));
$this->expectValid($schema);
}
// rejects a Union type with empty types
public function testRejectsAUnionTypeWithEmptyTypes()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: BadUnion
}
union BadUnion
'));
$this->expectInvalid($schema, [
[
'message' => 'Union type BadUnion must define one or more member types.',
'locations' => [locationShorthandToArray([5, 1])]
]
]);
}
// rejects a Union type with duplicated member type
public function testRejectsAUnionTypeWithDuplicatedMemberTypes()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: BadUnion
}
type TypeA {
field: String
}
type TypeB {
field: String
}
union BadUnion =
| TypeA
| TypeB
| TypeA
'));
$this->expectInvalid($schema, [
[
'message' => 'Union type BadUnion can only include type TypeA once.',
'locations' => locationsShorthandToArray([[14, 5], [16, 5]])
]
]);
}
// rejects a Union type with non-Object members types
public function testRejectsAUnionTypeWithNonObjectMemberTypes()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: BadUnion
}
type TypeA {
field: String
}
type TypeB {
field: String
}
union BadUnion =
| TypeA
| String
| TypeB
'));
$this->expectInvalid($schema, [
[
'message' => 'Union type BadUnion can only include Object types, it cannot include String.',
'locations' => [locationShorthandToArray([15, 5])],
]
]);
/** @noinspection PhpUnhandledExceptionInspection */
$badUnionMemberTypes = [
stringType(),
newNonNull($this->someObjectType),
newList($this->someObjectType),
$this->someInterfaceType,
$this->someUnionType,
$this->someEnumType,
$this->someInputObjectType,
];
foreach ($badUnionMemberTypes as $memberType) {
/** @noinspection PhpUnhandledExceptionInspection */
$badSchema = $this->schemaWithFieldType(
newUnionType(['name' => 'BadUnion', 'types' => [$memberType]])
);
$this->expectInvalid($badSchema, [
[
'message' => sprintf(
'Union type BadUnion can only include Object types, it cannot include %s.',
(string)$memberType
),
]
]);
}
}
// Type System: Input Objects must have fields
// accepts an Input Object type with fields
public function testAcceptsAnInputObjectTypeWithFields()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
field(arg: SomeInputObject): String
}
input SomeInputObject {
field: String
}
'));
$this->expectValid($schema);
}
// rejects an Input Object type with missing fields
public function testRejectsAnInputObjectTypeWithMissingFields()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
field(arg: SomeInputObject): String
}
input SomeInputObject
'));
$this->expectInvalid($schema, [
[
'message' => 'Input Object type SomeInputObject must define one or more fields.',
'locations' => [locationShorthandToArray([5, 1])],
]
]);
}
// rejects an Input Object type with incorrectly typed fields
public function testRejectsAnInputObjectTypeWithIncorrectlyTypedFields()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
field(arg: SomeInputObject): String
}
type SomeObject {
field: String
}
union SomeUnion = SomeObject
input SomeInputObject {
badObject: SomeObject
badUnion: SomeUnion
goodInputObject: SomeInputObject
}
'));
$this->expectInvalid($schema, [
[
'message' => 'The type of SomeInputObject.badObject must be Input Type but got: SomeObject.',
'locations' => [locationShorthandToArray([12, 3])],
],
[
'message' => 'The type of SomeInputObject.badUnion must be Input Type but got: SomeUnion.',
'locations' => [locationShorthandToArray([13, 3])],
]
]);
}
// Type System: Enum types must be well defined
// rejects an Enum type without values
public function testRejectsAnEnumTypeWithoutValues()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
field: SomeEnum
}
enum SomeEnum
'));
$this->expectInvalid($schema, [
[
'message' => 'Enum type SomeEnum must define one or more values.',
'locations' => [locationShorthandToArray([5, 1])],
]
]);
}
// rejects an Enum type with duplicate values
public function testRejectsAnEnumTypeWithDuplicateValues()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
field: SomeEnum
}
enum SomeEnum {
SOME_VALUE
SOME_VALUE
}
'));
$this->expectInvalid($schema, [
[
'message' => 'Enum type SomeEnum can include value SOME_VALUE only once.',
'locations' => locationsShorthandToArray([[6, 3], [7, 3]]),
]
]);
}
// rejects an Enum type with incorrectly named values
public function testRejectsAnEnumTypeWithIncorrectlyNamedValues()
{
$schemaWithEnum = function ($name) {
return $this->schemaWithFieldType(
newEnumType([
'name' => 'SomeEnum',
'values' => [
$name => [],
],
])
);
};
$badEnumValues = ['#value', '1value', 'KEBAB-CASE'];
foreach ($badEnumValues as $enumValue) {
$this->expectInvalid($schemaWithEnum($enumValue), [
[
'message' => sprintf('Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "%s" does not.', $enumValue)
]
]);
}
$forbiddenEnumValues = ['true', 'false', 'null'];
foreach ($forbiddenEnumValues as $enumValue) {
$this->expectInvalid($schemaWithEnum($enumValue), [
[
'message' => sprintf('Enum type SomeEnum cannot include value: %s.', $enumValue)
]
]);
}
}
// Type System: Object fields must have output types
// accepts an output type as an Object field type: ${type}
public function testAcceptsOutputTypesAsObjectFieldTypes()
{
foreach ($this->outputTypes as $outputType) {
$schema = $this->schemaWithFieldType(
$this->objectWithFieldOfType($outputType)
);
$this->expectValid($schema);
}
}
// rejects an empty Object field type
public function testRejectsAnEmptyObjectFieldType()
{
$schema = $this->schemaWithFieldType(
$this->objectWithFieldOfType(null)
);
$this->expectInvalid($schema, [
[
'message' => 'The type of BadObject.badField must be Output Type but got: null.'
]
]);
}
// rejects a non-output type as an Object field type: ${type}
public function testRejectsNonOutputTypeAsObjectFieldTypes()
{
foreach ($this->noOutputTypes as $notOutputType) {
$schema = $this->schemaWithFieldType(
$this->objectWithFieldOfType($notOutputType)
);
$this->expectInvalid($schema, [
[
'message' => \sprintf(
'The type of BadObject.badField must be Output Type but got: %s.',
toString($notOutputType)
)
]
]);
}
}
// rejects with relevant locations for a non-output type as an Object field type
public function testRejectsWithLocationsForANonOutputTypeAsAnObjectFieldType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
field: [SomeInputObject]
}
input SomeInputObject {
field: String
}
'));
$this->expectInvalid($schema, [
[
'message' => 'The type of Query.field must be Output Type but got: [SomeInputObject].',
'locations' => [locationShorthandToArray([2, 10])]
]
]);
}
// Type System: Objects can only implement unique interfaces
// rejects an Object implementing a non-type values
public function testRejectsAnObjectImplementingANonTypeValues()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'BadObject',
'fields' => ['f' => ['type' => stringType()]],
'interfaces' => [null],
]),
]);
$this->expectInvalid($schema, [
[
'message' => 'Type BadObject must only implement Interface types, it cannot implement null.'
]
]);
}
// rejects an Object implementing a non-Interface type
public function testRejectsAnObjectImplementingANonInterfaceType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: BadObject
}
input SomeInputObject {
field: String
}
type BadObject implements SomeInputObject {
field: String
}
'));
$this->expectInvalid($schema, [
[
'message' => 'Type BadObject must only implement Interface types, it cannot implement SomeInputObject.',
'locations' => [locationShorthandToArray([9, 27])]
]
]);
}
// rejects an Object implementing the same interface twice
public function testRejectsAnObjectImplementingTheSameInterfaceTwice()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field: String
}
type AnotherObject implements AnotherInterface & AnotherInterface {
field: String
}
'));
$this->expectInvalid($schema, [
[
'message' => 'Type AnotherObject can only implement AnotherInterface once.',
'locations' => locationsShorthandToArray([[9, 31], [9, 50]]),
]
]);
}
// rejects an Object implementing the same interface twice due to extension
public function testRejectsAnObjectImplementingTheSameInterfaceTwiceDueToExtension()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field: String
}
type AnotherObject implements AnotherInterface {
field: String
}
'));
/** @noinspection PhpUnhandledExceptionInspection */
$extendedSchema = extendSchema(
$schema,
'extend type AnotherObject implements AnotherInterface'
);
$this->expectInvalid($extendedSchema, [
[
'message' => 'Type AnotherObject can only implement AnotherInterface once.',
'locations' => locationsShorthandToArray([[9, 31], [1, 38]]),
]
]);
}
// Type System: Interface extensions should be valid
// rejects an Object implementing the extended interface due to missing field
public function testRejectsAnObjectImplementingTheExtendedInterfaceDueToMissingField()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field: String
}
type AnotherObject implements AnotherInterface {
field: String
}
'));
/** @noinspection PhpUnhandledExceptionInspection */
$extendedSchema = extendSchema(
$schema,
dedent('
extend interface AnotherInterface {
newField: String
}
')
);
$this->expectInvalid($extendedSchema, [
[
'message' =>
'Interface field AnotherInterface.newField expected ' .
'but AnotherObject does not provide it.',
'locations' => locationsShorthandToArray([[2, 3], [9, 1]]),
]
]);
}
// rejects an Object implementing the extended interface due to missing field args
public function testRejectsAnObjectImplementingTheExtendedInterfaceDueToMissingFieldArguments()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field: String
}
type AnotherObject implements AnotherInterface {
field: String
}
'));
/** @noinspection PhpUnhandledExceptionInspection */
$extendedSchema = extendSchema(
$schema,
dedent('
extend interface AnotherInterface {
newField(test: Boolean): String
}
extend type AnotherObject {
newField: String
}
')
);
$this->expectInvalid($extendedSchema, [
[
'message' =>
'Interface field argument AnotherInterface.newField(test:) expected ' .
'but AnotherObject.newField does not provide it.',
'locations' => locationsShorthandToArray([[2, 12], [6, 3]]),
]
]);
}
// rejects Objects implementing the extended interface due to mismatching interface type
public function testRejectsObjectsImplementingTheExtendedInterfaceDueToMisMatchingInterfaceType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field: String
}
type AnotherObject implements AnotherInterface {
field: String
}
'));
/** @noinspection PhpUnhandledExceptionInspection */
$extendedSchema = extendSchema(
$schema,
dedent('
extend interface AnotherInterface {
newInterfaceField: NewInterface
}
interface NewInterface {
newField: String
}
interface MismatchingInterface {
newField: String
}
extend type AnotherObject {
newInterfaceField: MismatchingInterface
}
')
);
$this->expectInvalid($extendedSchema, [
[
'message' =>
'Interface field AnotherInterface.newInterfaceField expects type NewInterface but ' .
'AnotherObject.newInterfaceField is type MismatchingInterface.',
'locations' => locationsShorthandToArray([[2, 22], [14, 22]]),
]
]);
}
// Type System: Interface fields must have output types
// accepts an output type as an Interface field type: ${type}
public function testAcceptsOutputTypesAsInterfaceFieldTypes()
{
foreach ($this->outputTypes as $outputType) {
$schema = $this->schemaWithFieldType(
$this->interfaceWithFieldOfType($outputType)
);
$this->expectValid($schema);
}
}
// rejects an empty Interface field type
public function testRejectsAnEmptyInterfaceFieldType()
{
$schema = $this->schemaWithFieldType(
$this->interfaceWithFieldOfType(null)
);
$this->expectInvalid($schema, [
[
'message' => 'The type of BadInterface.badField must be Output Type but got: null.'
]
]);
}
// rejects a non-output type as an Interface field type: ${type}
public function testRejectsNonOutputTypesAsInterfaceFieldTypes()
{
foreach ($this->noOutputTypes as $notOutputType) {
$schema = $this->schemaWithFieldType(
$this->interfaceWithFieldOfType($notOutputType)
);
$this->expectInvalid($schema, [
[
'message' => \sprintf(
'The type of BadInterface.badField must be Output Type but got: %s.',
toString($notOutputType)
)
]
]);
}
}
// rejects a non-output type as an Interface field type with locations
public function testRejectsWithLocationsForANonOutputTypeAsAnInterfaceFieldType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: SomeInterface
}
interface SomeInterface {
field: SomeInputObject
}
input SomeInputObject {
foo: String
}
'));
$this->expectInvalid($schema, [
[
'message' => 'The type of SomeInterface.field must be Output Type but got: SomeInputObject.',
'locations' => [locationShorthandToArray([6, 10])]
]
]);
}
// Type System: Field arguments must have input types
// accepts an input type as a field arg type: ${type}
public function testAcceptsInputTypesAsFieldArgumentTypes()
{
foreach ($this->inputTypes as $inputType) {
$schema = $this->schemaWithFieldType(
$this->objectWithFieldArgumentOfType($inputType)
);
$this->expectValid($schema);
}
}
// rejects an empty field arg type
public function testRejectsAnEmptyFieldArgumentType()
{
$schema = $this->schemaWithFieldType(
$this->objectWithFieldArgumentOfType(null)
);
$this->expectInvalid($schema, [
[
'message' => 'The type of BadObject.badField(badArg:) must be Input Type but got: null.'
]
]);
}
// rejects a non-input type as a field arg type: ${type}
public function testRejectsNonInputTypesAsFieldArgumentTypes()
{
foreach ($this->noInputTypes as $notInputType) {
$schema = $this->schemaWithFieldType(
$this->objectWithFieldArgumentOfType($notInputType)
);
$this->expectInvalid($schema, [
[
'message' => \sprintf(
'The type of BadObject.badField(badArg:) must be Input Type but got: %s.',
toString($notInputType)
)
]
]);
}
}
// rejects a non-input type as a field arg with locations
public function testRejectsWithLocationsForANonInputTypeAsAFieldArgumentType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test(arg: SomeObject): String
}
type SomeObject {
foo: String
}
'));
$this->expectInvalid($schema, [
[
'message' => 'The type of Query.test(arg:) must be Input Type but got: SomeObject.',
'locations' => [locationShorthandToArray([2, 8])]
]
]);
}
// Type System: Input Object fields must have input types
// accepts an input type as an input field type: ${type}
public function testAcceptsInputTypesAsInputFieldTypes()
{
foreach ($this->inputTypes as $inputType) {
$schema = $this->schemaWithFieldType(
$this->inputObjectWithFieldOfType($inputType)
);
$this->expectValid($schema);
}
}
// rejects an empty input field type
public function testRejectsAnEmptyInputFieldType()
{
$schema = $this->schemaWithFieldType(
$this->inputObjectWithFieldOfType(null)
);
$this->expectInvalid($schema, [
[
'message' => 'The type of BadInputObject.badField must be Input Type but got: null.'
]
]);
}
// rejects a non-input type as an input field type: ${type}
public function testRejectsNonInputTypesAsInputFieldTypes()
{
foreach ($this->noInputTypes as $notInputType) {
$schema = $this->schemaWithFieldType(
$this->inputObjectWithFieldOfType($notInputType)
);
$this->expectInvalid($schema, [
[
'message' => \sprintf(
'The type of BadInputObject.badField must be Input Type but got: %s.',
toString($notInputType)
)
]
]);
}
}
// rejects a non-input type as an input object field with locations
public function testRejectsWithLocationsForANonInputTypeAsAInputFieldType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test(arg: SomeInputObject): String
}
input SomeInputObject {
foo: SomeObject
}
type SomeObject {
bar: String
}
'));
$this->expectInvalid($schema, [
[
'message' => 'The type of SomeInputObject.foo must be Input Type but got: SomeObject.',
'locations' => [locationShorthandToArray([6, 3])]
]
]);
}
// Objects must adhere to Interface they implement
// accepts an Object which implements an Interface
public function testAcceptsAnObjectWhichImplementsAnInterface()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field(input: String): String
}
type AnotherObject implements AnotherInterface {
field(input: String): String
}
'));
$this->expectValid($schema);
}
// accepts an Object which implements an Interface along with more fields
public function testAcceptAnObjectWhichImplementsAnInterfaceAlongWithMoreFields()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field(input: String): String
}
type AnotherObject implements AnotherInterface {
field(input: String): String
anotherField: String
}
'));
$this->expectValid($schema);
}
// accepts an Object which implements an Interface field along with additional optional arguments
public function testAcceptsAnObjectWhichImplementsAnInterfaceFieldAlongWithAdditionalOptionalArguments()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field(input: String): String
}
type AnotherObject implements AnotherInterface {
field(input: String, anotherInput: String): String
}
'));
$this->expectValid($schema);
}
// rejects an Object missing an Interface field
public function testRejectsAnObjectMissingAnInterfaceField()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field(input: String): String
}
type AnotherObject implements AnotherInterface {
anotherField: String
}
'));
$this->expectInvalid($schema, [
[
'message' => 'Interface field AnotherInterface.field expected but AnotherObject does not provide it.',
'locations' => locationsShorthandToArray([[6, 3], [9, 1]])
]
]);
}
// rejects an Object with an incorrectly typed Interface field
public function testRejectsAnObjectWithIncorrectlyTypedInterfaceField()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field(input: String): String
}
type AnotherObject implements AnotherInterface {
field(input: String): Int
}
'));
$this->expectInvalid($schema, [
[
'message' =>
'Interface field AnotherInterface.field expects ' .
'type String but AnotherObject.field is type Int.',
'locations' => locationsShorthandToArray([[6, 25], [10, 25]])
]
]);
}
// rejects an Object with a differently typed Interface field
public function testRejectsAnObjectWithADifferentlyTypedInterfaceField()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
type A { foo: String }
type B { foo: String }
interface AnotherInterface {
field: A
}
type AnotherObject implements AnotherInterface {
field: B
}
'));
$this->expectInvalid($schema, [
[
'message' => 'Interface field AnotherInterface.field expects type A but AnotherObject.field is type B.',
'locations' => locationsShorthandToArray([[9, 10], [13, 10]])
]
]);
}
// accepts an Object with a subtyped Interface field (interface)
public function testAcceptsAnObjectWithASubtypedInterfaceField()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field: AnotherInterface
}
type AnotherObject implements AnotherInterface {
field: AnotherObject
}
'));
$this->expectValid($schema);
}
// accepts an Object with a subtyped Interface field (union)
public function testAcceptAnObjectWithASubtypedInterfaceField()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
type SomeObject {
field: String
}
union SomeUnionType = SomeObject
interface AnotherInterface {
field: SomeUnionType
}
type AnotherObject implements AnotherInterface {
field: SomeObject
}
'));
$this->expectValid($schema);
}
// rejects an Object missing an Interface argument
public function testRejectsAnObjectMissingAnInterfaceArgument()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field(input: String): String
}
type AnotherObject implements AnotherInterface {
field: String
}
'));
$this->expectInvalid($schema, [
[
'message' =>
'Interface field argument AnotherInterface.field(input:) expected ' .
'but AnotherObject.field does not provide it.',
'locations' => locationsShorthandToArray([[6, 9], [10, 3]])
]
]);
}
// rejects an Object with an incorrectly typed Interface argument
public function testRejectsAnObjectWithAnIncorrectlyTypedInterfaceArgument()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field(input: String): String
}
type AnotherObject implements AnotherInterface {
field(input: Int): String
}
'));
$this->expectInvalid($schema, [
[
'message' =>
'Interface field argument AnotherInterface.field(input:) expects ' .
'type String but AnotherObject.field(input:) is type Int.',
'locations' => locationsShorthandToArray([[6, 16], [10, 16]])
]
]);
}
// rejects an Object with both an incorrectly typed field and argument
public function testRejectsAnObjectWithBothAnIncorrectlyTypedFieldAndArgument()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field(input: String): String
}
type AnotherObject implements AnotherInterface {
field(input: Int): Int
}
'));
$this->expectInvalid($schema, [
[
'message' =>
'Interface field AnotherInterface.field expects type String but ' .
'AnotherObject.field is type Int.',
'locations' => locationsShorthandToArray([[6, 25], [10, 22]])
],
[
'message' =>
'Interface field argument AnotherInterface.field(input:) expects ' .
'type String but AnotherObject.field(input:) is type Int.',
'locations' => locationsShorthandToArray([[6, 16], [10, 16]])
]
]);
}
// rejects an Object which implements an Interface field along with additional required arguments
public function testRejectsAnObjectWhichImplementsAnInterfaceFieldAlongWithAdditionalRequiredArguments()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field(input: String): String
}
type AnotherObject implements AnotherInterface {
field(input: String, anotherInput: String!): String
}
'));
$this->expectInvalid($schema, [
[
'message' =>
'Object field argument AnotherObject.field(anotherInput:) is of ' .
'required type String! but is not also provided by the Interface ' .
'field AnotherInterface.field.',
'locations' => locationsShorthandToArray([[10, 24], [6, 3]])
]
]);
}
// accepts an Object with an equivalently wrapped Interface field type
public function testAcceptsAnObjectWithAnEquivalentlyWrappedInterfaceFieldType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field: [String]!
}
type AnotherObject implements AnotherInterface {
field: [String]!
}
'));
$this->expectValid($schema);
}
// rejects an Object with a non-list Interface field list type
public function testRejectsAnObjectWithANonListInterfaceFieldListType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field: [String]
}
type AnotherObject implements AnotherInterface {
field: String
}
'));
$this->expectInvalid($schema, [
[
'message' =>
'Interface field AnotherInterface.field expects type [String] ' .
'but AnotherObject.field is type String.',
'locations' => locationsShorthandToArray([[6, 10], [10, 10]])
]
]);
}
// rejects an Object with a list Interface field non-list type
public function testRejectsAnObjectWithAListInterfaceFieldNonListType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field: String
}
type AnotherObject implements AnotherInterface {
field: [String]
}
'));
$this->expectInvalid($schema, [
[
'message' =>
'Interface field AnotherInterface.field expects type String but ' .
'AnotherObject.field is type [String].',
'locations' => locationsShorthandToArray([[6, 10], [10, 10]])
]
]);
}
// accepts an Object with a subset non-null Interface field type
public function testAcceptsAnObjectWithASubsetNonNullInterfaceFieldType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field: String
}
type AnotherObject implements AnotherInterface {
field: String!
}
'));
$this->expectValid($schema);
}
// rejects an Object with a superset nullable Interface field type
public function testRejectsAnObjectWithASupersetNullableInterfaceFieldType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = buildSchema(dedent('
type Query {
test: AnotherObject
}
interface AnotherInterface {
field: String!
}
type AnotherObject implements AnotherInterface {
field: String
}
'));
$this->expectInvalid($schema, [
[
'message' =>
'Interface field AnotherInterface.field expects type String! ' .
'but AnotherObject.field is type String.',
'locations' => locationsShorthandToArray([[6, 10], [10, 10]])
]
]);
}
protected function expectValid($schema)
{
$errors = $this->schemaValidator->validate($schema);
$this->assertEquals([], $errors);
}
protected function expectInvalid($schema, $expectedErrors)
{
$errors = $this->schemaValidator->validate($schema);
$this->assertArraySubset($expectedErrors, \array_map(function (ValidationExceptionInterface $error) {
return $error->toArray();
}, $errors));
}
protected function withModifiers($types)
{
return \array_merge(
$types,
array_map(function ($type) {
return newList($type);
}, $types),
array_map(function ($type) {
return newNonNull($type);
}, $types),
array_map(function ($type) {
return newNonNull(newList($type));
}, $types)
);
}
protected function schemaWithFieldType($fieldType)
{
/** @noinspection PhpUnhandledExceptionInspection */
return newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => ['f' => ['type' => $fieldType]]
]),
'types' => [$fieldType],
]);
}
protected function objectWithFieldOfType($fieldType)
{
/** @noinspection PhpUnhandledExceptionInspection */
return newObjectType([
'name' => 'BadObject',
'fields' => [
'badField' => ['type' => $fieldType],
],
]);
}
protected function interfaceWithFieldOfType($fieldType)
{
/** @noinspection PhpUnhandledExceptionInspection */
return newInterfaceType([
'name' => 'BadInterface',
'fields' => [
'badField' => ['type' => $fieldType],
],
]);
}
protected function objectWithFieldArgumentOfType($argumentType)
{
/** @noinspection PhpUnhandledExceptionInspection */
return newObjectType([
'name' => 'BadObject',
'fields' => [
'badField' => [
'type' => stringType(),
'args' => [
'badArg' => ['type' => $argumentType],
],
],
],
]);
}
protected function inputObjectWithFieldOfType($fieldType)
{
/** @noinspection PhpUnhandledExceptionInspection */
return newObjectType([
'name' => 'BadObject',
'fields' => [
'badField' => [
'type' => stringType(),
'args' => [
'badArg' => [
'type' => newInputObjectType([
'name' => 'BadInputObject',
'fields' => [
'badField' => ['type' => $fieldType],
],
])
],
],
],
],
]);
}
}
================================================
FILE: tests/Functional/Type/DefinitionTest.php
================================================
blogImage = newObjectType([
'name' => 'Image',
'fields' => [
'url' => ['type' => stringType()],
'width' => ['type' => intType()],
'height' => ['type' => intType()],
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->blogAuthor = newObjectType([
'name' => 'Author',
'fields' => function () {
return [
'id' => ['type' => stringType()],
'name' => ['type' => stringType()],
'pic' => [
'type' => $this->blogImage,
'args' => [
'width' => ['type' => intType()],
'height' => ['type' => intType()],
],
],
'recentArticle' => ['type' => $this->blogArticle],
];
},
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->blogArticle = newObjectType([
'name' => 'Article',
'fields' => [
'id' => ['type' => stringType()],
'isPublished' => ['type' => booleanType()],
'author' => ['type' => $this->blogAuthor],
'title' => ['type' => stringType()],
'body' => ['type' => stringType()],
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->blogQuery = newObjectType([
'name' => 'Query',
'fields' => [
'article' => [
'args' => ['id' => ['type' => stringType()]],
'type' => $this->blogArticle,
],
'feed' => [
'type' => newList($this->blogArticle),
],
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->blogMutation = newObjectType([
'name' => 'Mutation',
'fields' => [
'writeArticle' => [
'type' => $this->blogArticle,
],
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->blogSubscription = newObjectType([
'name' => 'Subscription',
'fields' => [
'articleSubscribe' => [
'args' => ['id' => ['type' => stringType()]],
'type' => $this->blogArticle,
],
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->objectType = newObjectType(['name' => 'Object']);
/** @noinspection PhpUnhandledExceptionInspection */
$this->interfaceType = newInterfaceType(['name' => 'Interface']);
/** @noinspection PhpUnhandledExceptionInspection */
$this->unionType = newUnionType(['name' => 'Union', 'types' => [$this->objectType]]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->enumType = newEnumType(['name' => 'Enum', 'values' => ['foo' => []]]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->inputObjectType = newInputObjectType(['name' => 'InputObject']);
/** @noinspection PhpUnhandledExceptionInspection */
$this->scalarType = newScalarType([
'name' => 'Scalar',
'serialize' => function () {
},
'parseValue' => function () {
},
'parseLiteral' => function () {
},
]);
}
/**
* @param TypeInterface $type
* @return Schema
* @throws InvariantException
*/
protected function schemaWithField(TypeInterface $type): Schema
{
/** @noinspection PhpUnhandledExceptionInspection */
return newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'field' => ['type' => $type],
],
]),
'types' => [$type],
]);
}
/**
* @param mixed $resolveValue
* @return Schema
* @throws InvariantException
*/
protected function schemaWithObjectWithFieldResolver($resolveValue): Schema
{
/** @noinspection PhpUnhandledExceptionInspection */
$badResolverType = newObjectType([
'name' => 'BadResolver',
'fields' => [
'badField' => [
'type' => stringType(),
'resolve' => $resolveValue
],
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
return newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'f' => ['type' => $badResolverType],
],
])
]);
}
// Type System: Example
// defines a query only schema
public function testQueryOnlySchema()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => $this->blogQuery,
]);
$this->assertEquals($this->blogQuery, $schema->getQueryType());
/** @noinspection PhpUnhandledExceptionInspection */
$articleField = $this->blogQuery->getFields()['article'];
$this->assertEquals($this->blogArticle, $articleField->getType());
$this->assertEquals($this->blogArticle->getName(), $articleField->getType()->getName());
$this->assertSame('article', $articleField->getName());
/** @var ObjectType $articleFieldType */
$articleFieldType = $articleField->getType();
/** @noinspection PhpUnhandledExceptionInspection */
$titleField = $articleFieldType->getFields()['title'];
$this->assertSame('title', $titleField->getName());
$this->assertSame(stringType(), $titleField->getType());
$this->assertSame('String', $titleField->getType()->getName());
/** @noinspection PhpUnhandledExceptionInspection */
$authorField = $articleFieldType->getFields()['author'];
/** @var ObjectType $authorFieldType */
$authorFieldType = $authorField->getType();
/** @noinspection PhpUnhandledExceptionInspection */
$recentArticleField = $authorFieldType->getFields()['recentArticle'];
$this->assertEquals($this->blogArticle, !$recentArticleField ?: $recentArticleField->getType());
/** @noinspection PhpUnhandledExceptionInspection */
$feedField = $this->blogQuery->getFields()['feed'];
/** @var ListType $feedFieldType */
$feedFieldType = $feedField->getType();
$this->assertEquals($this->blogArticle, $feedFieldType->getOfType());
$this->assertSame('feed', $feedField->getName());
}
// defines a mutation schema
public function testMutationSchema()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'mutation' => $this->blogMutation,
]);
$this->assertEquals($this->blogMutation, $schema->getMutationType());
/** @noinspection PhpUnhandledExceptionInspection */
$writeArticleField = $this->blogMutation->getFields()['writeArticle'];
$this->assertEquals($this->blogArticle, $writeArticleField->getType());
$this->assertSame('Article', $writeArticleField->getType()->getName());
$this->assertSame('writeArticle', $writeArticleField->getName());
}
// defines a subscription schema
public function testSubscriptionSchema()
{
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'subscription' => $this->blogSubscription,
]);
$this->assertEquals($this->blogSubscription, $schema->getSubscriptionType());
/** @noinspection PhpUnhandledExceptionInspection */
$articleSubscribeField = $this->blogSubscription->getFields()['articleSubscribe'];
$this->assertEquals($this->blogArticle, $articleSubscribeField->getType());
$this->assertSame('Article', $articleSubscribeField->getType()->getName());
$this->assertSame('articleSubscribe', $articleSubscribeField->getName());
}
// defines an enum type with deprecated value
public function testEnumTypeWithDeprecatedValue()
{
/** @noinspection PhpUnhandledExceptionInspection */
$enumWithDeprecatedValue = newEnumType([
'name' => 'EnumWithDeprecatedValue',
'values' => ['foo' => ['deprecationReason' => 'Just because']],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$enumValue = $enumWithDeprecatedValue->getValues()[0];
$this->assertSame('foo', $enumValue->getName());
$this->assertTrue($enumValue->isDeprecated());
$this->assertSame('Just because', $enumValue->getDeprecationReason());
$this->assertSame('foo', $enumValue->getValue());
}
// defines an enum type with a value of `null`
public function testEnumTypeWithAValueOfNull()
{
/** @noinspection PhpUnhandledExceptionInspection */
$enumTypeWithNullValue = newEnumType([
'name' => 'EnumWithNullValue',
'values' => ['NULL' => ['value' => null]],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$enumValue = $enumTypeWithNullValue->getValues()[0];
$this->assertSame('NULL', $enumValue->getName());
$this->assertNull($enumValue->getDescription());
$this->assertSame(false, $enumValue->isDeprecated());
$this->assertNull($enumValue->getValue());
$this->assertNull($enumValue->getAstNode());
}
// defines an object type with deprecated field
public function testObjectWithDeprecatedField()
{
/** @noinspection PhpUnhandledExceptionInspection */
$typeWithDeprecatedField = newObjectType([
'name' => 'foo',
'fields' => [
'bar' => [
'type' => stringType(),
'deprecationReason' => 'A terrible reason',
],
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$field = $typeWithDeprecatedField->getFields()['bar'];
$this->assertSame(stringType(), $field->getType());
$this->assertSame('A terrible reason', $field->getDeprecationReason());
$this->assertTrue($field->isDeprecated());
$this->assertSame('bar', $field->getName());
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEmpty($field->getArguments());
}
// includes nested input objects in the map
public function testIncludesNestedInputObjectsInSchemaTypeMap()
{
/** @noinspection PhpUnhandledExceptionInspection */
$nestedInputObject = newInputObjectType([
'name' => 'NestedInputObject',
'fields' => ['value' => ['type' => stringType()]],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$someInputObject = newInputObjectType([
'name' => 'SomeInputObject',
'fields' => ['nested' => ['type' => $nestedInputObject]],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$someMutation = newObjectType([
'name' => 'SomeMutation',
'fields' => [
'mutateSomething' => [
'type' => $this->blogArticle,
'args' => ['input' => ['type' => $someInputObject]],
],
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$someSubscription = newObjectType([
'name' => 'SomeSubscription',
'fields' => [
'subscribeToSomething' => [
'type' => $this->blogArticle,
'args' => ['input' => ['type' => $someInputObject]],
],
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => $this->blogQuery,
'mutation' => $someMutation,
'subscription' => $someSubscription,
]);
$this->assertEquals($nestedInputObject, $schema->getTypeMap()[$nestedInputObject->getName()]);
}
// includes interface possible types in the type map
public function testIncludesInterfacePossibleTypesInSchemaTypeMap()
{
/** @noinspection PhpUnhandledExceptionInspection */
$someInterface = newInterfaceType([
'name' => 'SomeInterface',
'fields' => [
'f' => ['type' => intType()],
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$someSubtype = newObjectType([
'name' => 'SomeSubtype',
'fields' => [
'f' => ['type' => intType()],
],
'interfaces' => [$someInterface],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => [
'iface' => ['type' => $someInterface],
],
]),
'types' => [$someSubtype],
]);
$this->assertEquals($someSubtype, $schema->getTypeMap()[$someSubtype->getName()]);
}
// includes interfaces' thunk subtypes in the type map
public function testIncludesInterfacesThunkSubtypesInTheSchemaTypeMap()
{
/** @noinspection PhpUnhandledExceptionInspection */
$someInterface = newInterfaceType([
'name' => 'SomeInterface',
'fields' => ['f' => ['type' => intType()]],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$someSubtype = newObjectType([
'name' => 'SomeSubtype',
'fields' => ['f' => ['type' => intType()]],
'interfaces' => function () use ($someInterface) {
return [$someInterface];
},
]);
/** @noinspection PhpUnhandledExceptionInspection */
$schema = newSchema([
'query' => newObjectType([
'name' => 'Query',
'fields' => ['iface' => ['type' => $someInterface]],
]),
'types' => [$someSubtype],
]);
$this->assertEquals($someSubtype, $schema->getTypeMap()[$someSubtype->getName()]);
}
// stringifies simple types
public function testStringifySimpleTypes()
{
$this->assertEquals(TypeNameEnum::INT, (string)intType());
$this->assertEquals('Article', (string)$this->blogArticle);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals('Interface', (string)newInterfaceType(['name' => 'Interface']));
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals('Union', (string)newUnionType(['name' => 'Union']));
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals('Enum', (string)newEnumType(['name' => 'Enum']));
$this->assertEquals(TypeNameEnum::INT, (string)intType());
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals('Int!', (string)newNonNull(intType()));
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals('[Int]!', (string)newNonNull(newList(intType())));
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals('[Int!]', (string)newList(newNonNull(intType())));
$this->assertEquals('[[Int]]', (string)newList(newList(intType())));
}
// identifies input types
/**
* @param TypeInterface|null $type
* @param mixed $answer
* @dataProvider identifiesInputTypesDataProvider
* @throws \Digia\GraphQL\Error\InvalidTypeException
*/
public function testIdentifiesInputTypes(?TypeInterface $type, $answer)
{
$this->assertEquals($answer, isOutputType($type));
$this->assertEquals($answer, isOutputType(newList($type)));
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals($answer, isOutputType(newNonNull($type)));
}
public function identifiesInputTypesDataProvider(): array
{
/** @noinspection PhpUnhandledExceptionInspection */
// We cannot use the class fields here because they do not get instantiated for data providers.
return [
[intType(), true],
[newObjectType(['name' => 'Object']), true],
[newInterfaceType(['name' => 'Interface']), true],
[newUnionType(['name' => 'Union']), true],
[newEnumType(['name' => 'Enum']), true],
[newInputObjectType(['name' => 'InputObjectType']), false],
];
}
// prohibits nesting NonNull inside NonNull
public function testProhibitsNestingNonNullInsideNonNull()
{
$this->expectException(InvariantException::class);
$this->expectExceptionMessage('Expected Int! to be a GraphQL nullable type.');
/** @noinspection PhpUnhandledExceptionInspection */
newNonNull(newNonNull(intType()));
$this->addToAssertionCount(1);
}
// allows a thunk for Union member types
public function testAllowsAThunkForUnionMemberTypes()
{
/** @noinspection PhpUnhandledExceptionInspection */
$union = newUnionType([
'name' => 'ThunkUnion',
'types' => function () {
return [$this->objectType];
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$types = $union->getTypes();
$this->assertCount(1, $types);
$this->assertEquals($this->objectType, $types[0]);
}
// does not mutate passed field definitions
public function testDoesNotMutatePassedFieldDefinitions()
{
$fields = [
'field1' => ['type' => stringType()],
'field2' => [
'type' => stringType(),
'args' => [
'id' => ['type' => stringType()],
],
],
];
/** @noinspection PhpUnhandledExceptionInspection */
$testObject1 = newObjectType([
'name' => 'Test1',
'fields' => $fields,
]);
/** @noinspection PhpUnhandledExceptionInspection */
$testObject2 = newObjectType([
'name' => 'Test2',
'fields' => $fields,
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals($testObject2->getFields(), $testObject1->getFields());
/** @noinspection PhpUnhandledExceptionInspection */
$testInputObject1 = newInputObjectType([
'name' => 'Test1',
'fields' => $fields,
]);
/** @noinspection PhpUnhandledExceptionInspection */
$testInputObject2 = newInputObjectType([
'name' => 'Test2',
'fields' => $fields,
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals($testInputObject2->getFields(), $testInputObject1->getFields());
$this->assertEquals(stringType(), $fields['field1']['type']);
$this->assertEquals(stringType(), $fields['field2']['type']);
$this->assertEquals(stringType(), $fields['field2']['args']['id']['type']);
}
// Field config must be object
// accepts an Object type with a field function
public function testAcceptsAnObjectTypeWithAFieldFunction()
{
/** @noinspection PhpUnhandledExceptionInspection */
$objectType = newObjectType([
'name' => 'SomeObject',
'fields' => function () {
return ['f' => ['type' => stringType()]];
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals(stringType(), $objectType->getField('f')->getType());
}
// rejects an Object type field with undefined config
public function testRejectsAnObjectTypeFieldWithNullConfig()
{
/** @noinspection PhpUnhandledExceptionInspection */
$objType = newObjectType([
'name' => 'SomeObject',
'fields' => ['f' => null],
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage('SomeObject.f field config must be an associative array.');
/** @noinspection PhpUnhandledExceptionInspection */
$objType->getFields();
$this->addToAssertionCount(1);
}
// rejects an Object type with incorrectly typed fields
function testRejectsAnObjectTypeWithIncorrectlyTypedFields()
{
/** @noinspection PhpUnhandledExceptionInspection */
$objType = newObjectType([
'name' => 'SomeObject',
'fields' => [['f' => null]],
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(
'SomeObject fields must be an associative array with field names as keys or a ' .
'callable which returns such an array.'
);
/** @noinspection PhpUnhandledExceptionInspection */
$objType->getFields();
$this->addToAssertionCount(1);
}
// rejects an Object type with a field function that returns incorrect type
public function testRejectsAnObjectTypeWithAFieldFunctionThatReturnsIncorrectType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$objType = newObjectType([
'name' => 'SomeObject',
'fields' => function () {
return [['f' => null]];
},
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(
'SomeObject fields must be an associative array with field names as keys or a ' .
'callable which returns such an array.'
);
/** @noinspection PhpUnhandledExceptionInspection */
$objType->getFields();
$this->addToAssertionCount(1);
}
// Field arg config must be object
// accepts an Object type with field args
public function testAcceptsAnObjectTypeWithFieldArgs()
{
/** @noinspection PhpUnhandledExceptionInspection */
$objType = newObjectType([
'name' => 'SomeObject',
'fields' => [
'goodField' => [
'type' => stringType(),
'args' => [
'goodArg' => ['type' => stringType()],
],
],
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$objType->getFields();
$this->addToAssertionCount(1);
}
// rejects an Object type with incorrectly typed field args
public function testRejectsAnObjectTypeWithIncorrectlyTypedFieldArgs()
{
/** @noinspection PhpUnhandledExceptionInspection */
$objType = newObjectType([
'name' => 'SomeObject',
'fields' => [
'badField' => [
'type' => stringType(),
'args' => [['badArg' => stringType()]],
],
],
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage('SomeObject.badField args must be an object with argument names as keys.');
/** @noinspection PhpUnhandledExceptionInspection */
$objType->getFields();
$this->addToAssertionCount(1);
}
// does not allow isDeprecated without deprecationReason on field
public function testDoesNotAllowIsDeprecatedWithoutDeprecationReasonOnField()
{
/** @noinspection PhpUnhandledExceptionInspection */
$oldObject = newObjectType([
'name' => 'OldObject',
'fields' => [
'field' => [
'type' => stringType(),
'isDeprecated' => true,
],
],
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage('OldObject.field should provide "deprecationReason" instead of "isDeprecated".');
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField($oldObject);
$this->addToAssertionCount(1);
}
// Object interfaces must be array
// accepts an Object type with array interfaces
public function testAcceptsAnObjectTypeWithArrayInterfaces()
{
/** @noinspection PhpUnhandledExceptionInspection */
$objType = newObjectType([
'name' => 'SomeObject',
'interfaces' => [$this->interfaceType],
'fields' => ['f' => ['type' => stringType()]],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals($this->interfaceType, $objType->getInterfaces()[0]);
}
// accepts an Object type with interfaces as a function returning an array
public function testAcceptsAnObjectTypeWithInterfacesAsAFunctionReturningAnArray()
{
/** @noinspection PhpUnhandledExceptionInspection */
$objType = newObjectType([
'name' => 'SomeObject',
'interfaces' => [$this->interfaceType],
'fields' => [
'f' => function () {
return ['type' => stringType()];
},
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals($this->interfaceType, $objType->getInterfaces()[0]);
}
// rejects an Object type with incorrectly typed interfaces
public function testRejectsAnObjectTypeWithIncorrectlyTypedInterfaces()
{
/** @noinspection PhpUnhandledExceptionInspection */
$objType = newObjectType([
'name' => 'SomeObject',
'interfaces' => '',
'fields' => ['f' => ['type' => stringType()]],
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage('SomeObject interfaces must be an array or a function which returns an array.');
/** @noinspection PhpUnhandledExceptionInspection */
$objType->getInterfaces();
}
// rejects an Object type with interfaces as a function returning an incorrect type
public function testRejectsAnObjectTypeWithInterfacesAsAFunctionReturningAnIncorrectType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$objType = newObjectType([
'name' => 'SomeObject',
'interfaces' => function () {
return '';
},
'fields' => ['f' => ['type' => stringType()]],
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage('SomeObject interfaces must be an array or a function which returns an array.');
/** @noinspection PhpUnhandledExceptionInspection */
$objType->getInterfaces();
}
// Type System: Object fields must have valid resolve values
// accepts a lambda as an Object field resolver
public function testAcceptsALambdaAsAnObjectFieldResolver()
{
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithObjectWithFieldResolver(function () {
return [];
});
$this->addToAssertionCount(1);
}
// rejects an empty Object field resolver
public function testRejectsAnEmptyArrayFieldResolver()
{
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(
'BadResolver.badField field resolver must be a function if provided, but got: Array.'
);
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithObjectWithFieldResolver([]);
$this->addToAssertionCount(1);
}
// ejects a constant scalar value resolver
public function testRejectsAnScalarFieldResolver()
{
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(
'BadResolver.badField field resolver must be a function if provided, but got: 0.'
);
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithObjectWithFieldResolver(0);
$this->addToAssertionCount(1);
}
// Type System: Interface types must be resolvable
// accepts an Interface type defining resolveType
public function testAcceptsAnInterfaceTypeDefiningResolveType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$anotherInterfaceType = newInterfaceType([
'name' => 'AnotherInterface',
'fields' => ['f' => ['type' => stringType()]],
'resolveType' => function () {
return '';
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newObjectType([
'name' => 'SomeObject',
'interfaces' => [$anotherInterfaceType],
'fields' => ['f' => ['type' => stringType()]],
])
);
$this->addToAssertionCount(1);
}
// accepts an Interface with implementing type defining isTypeOf
public function testAcceptsAnInterfaceTypeWithImplementingTypeDefiningIsTypeOf()
{
/** @noinspection PhpUnhandledExceptionInspection */
$anotherInterfaceType = newInterfaceType([
'name' => 'AnotherInterface',
'fields' => ['f' => ['type' => stringType()]],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newObjectType([
'name' => 'SomeObject',
'interfaces' => [$anotherInterfaceType],
'fields' => ['f' => ['type' => stringType()]],
'isTypeOf' => function () {
return true;
},
])
);
$this->addToAssertionCount(1);
}
// accepts an Interface type defining resolveType with implementing type defining isTypeOf
public function testAcceptsAnInterfaceTypeDefiningResolveTypeWithImplementingTypeDefiningIsTypeOf()
{
/** @noinspection PhpUnhandledExceptionInspection */
$anotherInterfaceType = newInterfaceType([
'name' => 'AnotherInterface',
'fields' => ['f' => ['type' => stringType()]],
'resolveType' => function () {
return '';
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newObjectType([
'name' => 'SomeObject',
'interfaces' => [$anotherInterfaceType],
'fields' => ['f' => ['type' => stringType()]],
'isTypeOf' => function () {
return true;
},
])
);
$this->addToAssertionCount(1);
}
// rejects an Interface type with an incorrect type for resolveType
public function testRejectsAnInterfaceTypeWithAnIncorrectTypeForResolveType()
{
$this->expectException(InvariantException::class);
$this->expectExceptionMessage('AnotherInterface must provide "resolveType" as a function.');
/** @noinspection PhpUnhandledExceptionInspection */
newInterfaceType([
'name' => 'AnotherInterface',
'resolveType' => '',
'fields' => ['f' => ['type' => stringType()]],
]);
}
// Type System: Union types must be resolvable
// accepts a Union type defining resolveType
public function testAcceptsUnionTypeDefiningResolveType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newUnionType([
'name' => 'SomeUnion',
'types' => [$this->objectType],
])
);
$this->addToAssertionCount(1);
}
// accepts a Union of Object types defining isTypeOf
public function testAcceptsAUnionOfObjectTypesDefiningIsTypeOf()
{
/** @noinspection PhpUnhandledExceptionInspection */
$objectWithIsTypeOf = newObjectType([
'name' => 'ObjectWithIsTypeOf',
'fields' => ['f' => ['type' => stringType()]],
'isTypeOf' => function () {
return true;
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newUnionType([
'name' => 'SomeUnion',
'types' => [$objectWithIsTypeOf],
])
);
$this->addToAssertionCount(1);
}
// accepts a Union type defining resolveType of Object types defining isTypeOf
public function testAcceptsAUnionTypeDefiningResolveTypeOfObjectTypesDefiningIsTypeOf()
{
/** @noinspection PhpUnhandledExceptionInspection */
$objectWithIsTypeOf = newObjectType([
'name' => 'ObjectWithIsTypeOf',
'fields' => ['f' => ['type' => stringType()]],
'isTypeOf' => function () {
return true;
}
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newUnionType([
'name' => 'SomeUnion',
'types' => [$objectWithIsTypeOf],
'resolveType' => function () {
return 'ObjectWithIsTypeOf';
}
])
);
$this->addToAssertionCount(1);
}
// rejects an Interface type with an incorrect type for resolveType
public function testRejectsAnInterfaceWithAnIncorrectTypeForResolveType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$objectWithIsTypeOf = newObjectType([
'name' => 'ObjectWithIsTypeOf',
'fields' => ['f' => ['type' => stringType()]],
'isTypeOf' => function () {
return true;
}
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage('SomeUnion must provide "resolveType" as a function.');
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newUnionType([
'name' => 'SomeUnion',
'types' => [$objectWithIsTypeOf],
'resolveType' => ''
])
);
$this->addToAssertionCount(1);
}
// Type System: Scalar types must be serializable
// accepts a Scalar type defining serialize
public function testAcceptsAScalarTypeDefiningSerialize()
{
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newScalarType([
'name' => 'SomeScalar',
'serialize' => function () {
return null;
},
])
);
$this->addToAssertionCount(1);
}
// rejects a Scalar type not defining serialize
public function testRejectsAScalarTypeNotDefiningSerialize()
{
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(
'SomeScalar must provide "serialize" function. If this custom Scalar ' .
'is also used as an input type, ensure "parseValue" and "parseLiteral" ' .
'functions are also provided.'
);
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newScalarType([
'name' => 'SomeScalar',
])
);
$this->addToAssertionCount(1);
}
// rejects a Scalar type defining serialize with an incorrect type
public function testRejectsAScalarTypeDefiningSerializeWithAnIncorrectType()
{
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(
'SomeScalar must provide "serialize" function. If this custom Scalar ' .
'is also used as an input type, ensure "parseValue" and "parseLiteral" ' .
'functions are also provided.'
);
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newScalarType([
'name' => 'SomeScalar',
'serialize' => '',
])
);
$this->addToAssertionCount(1);
}
// accepts a Scalar type defining parseValue and parseLiteral
public function testAcceptsAScalarTypeDefiningParseValueAndParseLiteral()
{
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newScalarType([
'name' => 'SomeScalar',
'serialize' => function () {
return null;
},
'parseValue' => function () {
return null;
},
'parseLiteral' => function () {
return null;
},
])
);
$this->addToAssertionCount(1);
}
// rejects a Scalar type defining parseValue but not parseLiteral
public function testRejectsAScalarTypeDefiningParseValueButNotParseLiteral()
{
$this->expectException(InvariantException::class);
$this->expectExceptionMessage('SomeScalar must provide both "parseValue" and "parseLiteral" functions.');
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newScalarType([
'name' => 'SomeScalar',
'serialize' => function () {
return null;
},
'parseValue' => function () {
return null;
},
])
);
$this->addToAssertionCount(1);
}
// rejects a Scalar type defining parseLiteral but not parseValue
public function testRejectsAScalarTypeDefiningParseLiteralButNotParseValue()
{
$this->expectException(InvariantException::class);
$this->expectExceptionMessage('SomeScalar must provide both "parseValue" and "parseLiteral" functions.');
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newScalarType([
'name' => 'SomeScalar',
'serialize' => function () {
return null;
},
'parseLiteral' => function () {
return null;
},
])
);
$this->addToAssertionCount(1);
}
// rejects a Scalar type defining parseValue and parseLiteral with an incorrect type
public function testRejectsAScalarTypeDefiningParseValueAndParseLiteralWithAnIncorrectType()
{
$this->expectException(InvariantException::class);
$this->expectExceptionMessage('SomeScalar must provide both "parseValue" and "parseLiteral" functions.');
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newScalarType([
'name' => 'SomeScalar',
'serialize' => function () {
return null;
},
'parseValue' => '',
'parseLiteral' => '',
])
);
$this->addToAssertionCount(1);
}
// Type System: Object types must be assertable
// accepts an Object type with an isTypeOf function
public function testAcceptsAnObjectTypeWithAnIsTypeOfFunction()
{
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newObjectType([
'name' => 'AnotherObject',
'fields' => ['f' => ['type' => stringType()]],
'isTypeOf' => function () {
return true;
},
])
);
$this->addToAssertionCount(1);
}
// rejects an Object type with an incorrect type for isTypeOf
public function testRejectsAnObjectTypeWithAnIncorrectTypeForIsTypeOf()
{
$this->expectException(InvariantException::class);
$this->expectExceptionMessage('AnotherObject must provide "isTypeOf" as a function.');
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newObjectType([
'name' => 'AnotherObject',
'fields' => ['f' => ['type' => stringType()]],
'isTypeOf' => '',
])
);
$this->addToAssertionCount(1);
}
// Type System: Union types must be array
// accepts a Union type with array types
public function testAcceptsAnUnionTypeWithArrayTypes()
{
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newUnionType([
'name' => 'SomeUnion',
'types' => [$this->objectType],
])
);
$this->addToAssertionCount(1);
}
// accepts a Union type with function returning an array of types
public function testAcceptsAnUnionTypeWithFunctionReturningAnArrayOfTypes()
{
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newUnionType([
'name' => 'SomeUnion',
'types' => function () {
return [$this->objectType];
},
])
);
$this->addToAssertionCount(1);
}
// rejects a Union type without types
public function testRejectsAnUnionWithoutTypes()
{
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newUnionType([
'name' => 'SomeUnion',
])
);
$this->addToAssertionCount(1);
}
// rejects a Union type with incorrectly typed types
public function testRejectsAUnionTypeWithIncorrectlyTypedTypes()
{
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(
'Must provide array of types or a function which returns such an array for Union SomeUnion.'
);
/** @noinspection PhpUnhandledExceptionInspection */
$this->schemaWithField(
newUnionType([
'name' => 'SomeUnion',
'types' => ''
])
);
$this->addToAssertionCount(1);
}
// Type System: Input Objects must have fields
// accepts an Input Object type with fields
public function testAcceptsAnInputObjectTypeWithFields()
{
/** @noinspection PhpUnhandledExceptionInspection */
$inputObjectType = newInputObjectType([
'name' => 'SomeInputObject',
'fields' => ['f' => ['type' => stringType()]],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$field = $inputObjectType->getField('f');
$this->assertEquals(stringType(), $field->getType());
}
// accepts an Input Object type with a field function
public function testAcceptsAnInputObjectTypeWithAFieldFunction()
{
/** @noinspection PhpUnhandledExceptionInspection */
$inputObjectType = newInputObjectType([
'name' => 'SomeInputObject',
'fields' => function () {
return ['f' => ['type' => stringType()]];
},
]);
/** @noinspection PhpUnhandledExceptionInspection */
$field = $inputObjectType->getField('f');
$this->assertEquals(stringType(), $field->getType());
}
// rejects an Input Object type with incorrect fields
public function testRejectsAnInputObjectTypeWithIncorrectFields()
{
/** @noinspection PhpUnhandledExceptionInspection */
$inputObjectType = newInputObjectType([
'name' => 'SomeInputObject',
'fields' => '',
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(
'SomeInputObject fields must be an associative array with field names as keys or a ' .
'function which returns such an array.'
);
/** @noinspection PhpUnhandledExceptionInspection */
$inputObjectType->getField('f');
}
// rejects an Input Object type with fields function that returns incorrect type
public function testRejectsAnInputObjectTypeWithFieldsFunctionThatReturnsIncorrectType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$inputObjectType = newInputObjectType([
'name' => 'SomeInputObject',
'fields' => function () {
return '';
},
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(
'SomeInputObject fields must be an associative array with field names as keys or a ' .
'function which returns such an array.'
);
/** @noinspection PhpUnhandledExceptionInspection */
$inputObjectType->getField('f');
}
// Type System: Input Object fields must not have resolvers
// rejects an Input Object type with resolvers
public function testRejectsAnInputObjectTypeWithResolvers()
{
/** @noinspection PhpUnhandledExceptionInspection */
$inputObjectType = newInputObjectType([
'name' => 'SomeInputObject',
'fields' => [
'f' => [
'type' => stringType(),
'resolve' => function () {
return 0;
}
],
],
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(
'SomeInputObject.f field type has a resolve property, ' .
'but Input Types cannot define resolvers.'
);
/** @noinspection PhpUnhandledExceptionInspection */
$inputObjectType->getFields();
$this->addToAssertionCount(1);
}
// rejects an Input Object type with resolver constant
public function testRejectsAnInputObjectTypeWithIncorrectlyTypedResolver()
{
/** @noinspection PhpUnhandledExceptionInspection */
$inputObjectType = newInputObjectType([
'name' => 'SomeInputObject',
'fields' => [
'f' => [
'type' => stringType(),
'resolve' => ''
],
],
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(
'SomeInputObject.f field type has a resolve property, ' .
'but Input Types cannot define resolvers.'
);
/** @noinspection PhpUnhandledExceptionInspection */
$inputObjectType->getFields();
$this->addToAssertionCount(1);
}
// Type System: Enum types must be well defined
// accepts a well defined Enum type with empty value definition
public function testAcceptsAWellDefinedEnumTypeWithEmptyValueDefinition()
{
/** @noinspection PhpUnhandledExceptionInspection */
$enumType = newEnumType([
'name' => 'SomeEnum',
'values' => [
'FOO' => [],
'BAR' => [],
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals('FOO', $enumType->getValue('FOO')->getValue());
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals('BAR', $enumType->getValue('BAR')->getValue());
}
// accepts a well defined Enum type with internal value definition
public function testAcceptsAWellDefinedEnumTypeWithInternalValueDefinition()
{
/** @noinspection PhpUnhandledExceptionInspection */
$enumType = newEnumType([
'name' => 'SomeEnum',
'values' => [
'FOO' => ['value' => 10],
'BAR' => ['value' => 20],
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals(10, $enumType->getValue('FOO')->getValue());
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertEquals(20, $enumType->getValue('BAR')->getValue());
}
// rejects an Enum type with incorrectly typed values
public function testRejectsAnEnumWithIncorrectlyTypedValues()
{
/** @noinspection PhpUnhandledExceptionInspection */
$enumType = newEnumType([
'name' => 'SomeEnum',
'values' => [['FOO' => 10]],
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage('SomeEnum values must be an associative array with value names as keys.');
/** @noinspection PhpUnhandledExceptionInspection */
$enumType->getValues();
$this->addToAssertionCount(1);
}
// rejects an Enum type with missing value definition
public function testRejectsAnEnumTypeWithMissingValueDefinition()
{
/** @noinspection PhpUnhandledExceptionInspection */
$enumType = newEnumType([
'name' => 'SomeEnum',
'values' => ['FOO' => null],
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(
'SomeEnum.FOO must refer to an object with a "value" key representing ' .
'an internal value but got: null.'
);
/** @noinspection PhpUnhandledExceptionInspection */
$enumType->getValues();
$this->addToAssertionCount(1);
}
// rejects an Enum type with incorrectly typed value definition
public function testRejectsAnEnumTypeWithIncorrectlyTypedValueDefinition()
{
/** @noinspection PhpUnhandledExceptionInspection */
$enumType = newEnumType([
'name' => 'SomeEnum',
'values' => ['FOO' => 10],
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(
'SomeEnum.FOO must refer to an object with a "value" key representing ' .
'an internal value but got: 10.'
);
/** @noinspection PhpUnhandledExceptionInspection */
$enumType->getValues();
$this->addToAssertionCount(1);
}
// does not allow isDeprecated without deprecationReason on enum
public function testDoesNotAllowIsDeprecatedWithoutDeprecationReasonOnEnum()
{
/** @noinspection PhpUnhandledExceptionInspection */
$enumType = newEnumType([
'name' => 'SomeEnum',
'values' => ['FOO' => ['isDeprecated' => true]],
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(
'SomeEnum.FOO should provide "deprecationReason" instead of "isDeprecated".'
);
/** @noinspection PhpUnhandledExceptionInspection */
$enumType->getValues();
$this->addToAssertionCount(1);
}
// Type System: List must accept only types
// accepts an type as item type of list
public function testAcceptsTypesAsItemOfList()
{
/** @noinspection PhpUnhandledExceptionInspection */
$types = [
stringType(),
$this->scalarType,
$this->objectType,
$this->unionType,
$this->interfaceType,
$this->enumType,
$this->inputObjectType,
newList(stringType()),
newNonNull(stringType()),
];
foreach ($types as $type) {
/** @noinspection PhpUnhandledExceptionInspection */
newList($type);
$this->addToAssertionCount(1);
}
}
// rejects a non-type as item type of list
public function testRejectsANonTypesAsItemTypeOfList()
{
$notTypes = [
'Object' => new \stdClass(),
'Array' => [],
'Function' => function () {
return null;
},
'(empty string)' => '',
'null' => null,
'true' => true,
'false' => false,
'String' => 'String',
];
foreach ($notTypes as $string => $notType) {
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(\sprintf('Expected %s to be a GraphQL type.', $string));
/** @noinspection PhpUnhandledExceptionInspection */
newList($notType);
$this->addToAssertionCount(1);
}
}
// Type System: NonNull must only accept non-nullable types
// accepts an type as nullable type of non-null
public function testAcceptsTypesAsNullableTypeOfNonNull()
{
/** @noinspection PhpUnhandledExceptionInspection */
$types = [
stringType(),
$this->scalarType,
$this->objectType,
$this->unionType,
$this->interfaceType,
$this->enumType,
$this->inputObjectType,
newList(stringType()),
newList(newNonNull(stringType())),
];
foreach ($types as $type) {
/** @noinspection PhpUnhandledExceptionInspection */
newNonNull($type);
$this->addToAssertionCount(1);
}
}
// rejects a non-type as nullable type of non-null
public function testRejectsNonTypesAsNullableTypeOfNonNull()
{
/** @noinspection PhpUnhandledExceptionInspection */
$nonTypes = [
'String!' => newNonNull(stringType()),
'Object' => new \stdClass(),
'Array' => [],
'Function' => function () {
return null;
},
'(empty string)' => '',
'null' => null,
'true' => true,
'false' => false,
'String' => 'String',
];
foreach ($nonTypes as $string => $nonType) {
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(\sprintf('Expected %s to be a GraphQL nullable type.', $string));
/** @noinspection PhpUnhandledExceptionInspection */
newNonNull($nonType);
$this->addToAssertionCount(1);
}
}
// Type System: A Schema must contain uniquely named types
// rejects a Schema which redefines a built-in type
public function testRejectsASchemaWhichDefinesABuiltInType()
{
/** @noinspection PhpUnhandledExceptionInspection */
$fakeString = newScalarType([
'name' => 'String',
'serialize' => function () {
return null;
},
]);
/** @noinspection PhpUnhandledExceptionInspection */
$queryType = newObjectType([
'name' => 'Query',
'fields' => [
'normal' => ['type' => stringType()],
'fake' => ['type' => $fakeString],
]
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(
'Schema must contain unique named types but contains multiple types named "String".'
);
/** @noinspection PhpUnhandledExceptionInspection */
newSchema(['query' => $queryType]);
$this->addToAssertionCount(1);
}
// rejects a Schema which defines an object type twice
public function testRejectsASchemaWhichDefinesAnObjectTypeTwice()
{
/** @noinspection PhpUnhandledExceptionInspection */
$a = newObjectType([
'name' => 'SameName',
'fields' => ['f' => ['type' => stringType()]],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$b = newObjectType([
'name' => 'SameName',
'fields' => ['f' => ['type' => stringType()]],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$queryType = newObjectType([
'name' => 'Query',
'fields' => [
'a' => ['type' => $a],
'b' => ['type' => $b],
]
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(
'Schema must contain unique named types but contains multiple types named "SameName".'
);
/** @noinspection PhpUnhandledExceptionInspection */
newSchema(['query' => $queryType]);
$this->addToAssertionCount(1);
}
// rejects a Schema which have same named objects implementing an interface
public function testRejectsASchemaWhichHaveSameNamedObjectsImplementingAnInterface()
{
/** @noinspection PhpUnhandledExceptionInspection */
$anotherInterface = newInterfaceType([
'name' => 'AnotherInterface',
'fields' => ['f' => ['type' => stringType()]],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$firstBadObject = newObjectType([
'name' => 'BadObject',
'interfaces' => [$anotherInterface],
'fields' => ['f' => ['type' => stringType()]],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$secondBadObject = newObjectType([
'name' => 'BadObject',
'interfaces' => [$anotherInterface],
'fields' => ['f' => ['type' => stringType()]],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$queryType = newObjectType([
'name' => 'Query',
'fields' => [
'iface' => ['type' => $anotherInterface],
]
]);
$this->expectException(InvariantException::class);
$this->expectExceptionMessage(
'Schema must contain unique named types but contains multiple types named "BadObject".'
);
/** @noinspection PhpUnhandledExceptionInspection */
newSchema([
'query' => $queryType,
'types' => [$firstBadObject, $secondBadObject],
]);
$this->addToAssertionCount(1);
}
}
================================================
FILE: tests/Functional/Type/EnumTypeTest.php
================================================
colorType = newEnumType([
'name' => 'Color',
'values' => [
'RED' => ['value' => 0],
'GREEN' => ['value' => 1],
'BLUE' => ['value' => 2],
]
]);
$this->complex1 = [
'someRandomFunction' => function () {
},
];
$this->complex2 = [
'someRandomValue' => 123,
];
$this->complexEnum = newEnumType([
'name' => 'Complex',
'values' => [
'ONE' => ['value' => $this->complex1],
'TWO' => ['value' => $this->complex2],
],
]);
$this->queryType = newObjectType([
'name' => 'Query',
'fields' => [
'colorEnum' => [
'type' => $this->colorType,
'args' => [
'fromEnum' => ['type' => $this->colorType],
'fromInt' => ['type' => intType()],
'fromString' => ['type' => stringType()],
],
'resolve' => function ($value, $args) {
return $args['fromInt'] ?? $args['fromString'] ?? $args['fromEnum'];
},
],
'colorInt' => [
'type' => intType(),
'args' => [
'fromEnum' => ['type' => $this->colorType],
'fromInt' => ['type' => intType()],
],
'resolve' => function ($value, $args) {
return $args['fromInt'] ?? $args['fromEnum'];
},
],
'complexEnum' => [
'type' => $this->complexEnum,
'args' => [
'fromEnum' => [
'type' => $this->complexEnum,
'defaultValue' => $this->complex1,
],
'providedGoodValue' => ['type' => booleanType()],
'providedBadValue' => ['type' => booleanType()],
],
'resolve' => function ($value, $args) {
if ($args['providedGoodValue']) {
return $this->complex2;
}
if ($args['providedBadValue']) {
return ['someRandomValue' => 123];
}
return $args['fromEnum'];
}
],
],
]);
$this->mutationType = newObjectType([
'name' => 'Mutation',
'fields' => [
'favoriteEnum' => [
'type' => $this->colorType,
'args' => ['color' => ['type' => $this->colorType]],
'resolve' => function ($value, $args) {
return $args['color'];
},
],
],
]);
$this->subscriptionType = newObjectType([
'name' => 'Subscription',
'fields' => [
'subscribeToEnum' => [
'type' => $this->colorType,
'args' => ['color' => ['type' => $this->colorType]],
'resolve' => function ($value, $args) {
return $args['color'];
},
],
],
]);
$this->schema = newSchema([
'query' => $this->queryType,
'mutation' => $this->mutationType,
'subscription' => $this->subscriptionType,
]);
}
// TODO: Add missing tests when possible.
public function testPresentsAGetValuesAPIForComplexEnums()
{
$values = $this->complexEnum->getValues();
$this->assertEquals(2, count($values));
$this->assertEquals('ONE', $values[0]->getName());
$this->assertEquals($this->complex1, $values[0]->getValue());
$this->assertEquals('TWO', $values[1]->getName());
$this->assertEquals($this->complex2, $values[1]->getValue());
}
public function testPresentsAGetValueAPIForComplexEnums()
{
$oneValue = $this->complexEnum->getValue('ONE');
$this->assertEquals('ONE', $oneValue->getName());
$this->assertEquals($this->complex1, $oneValue->getValue());
}
// TODO: Add test for 'may be internally represented with complex values'.
// TODO: Add test for 'can be introspected without error'.
}
================================================
FILE: tests/Functional/Type/IntrospectionTest.php
================================================
introspectionQuery = readFileContents(__DIR__ . '/introspection.graphql');
}
public function testExecutesAnIntrospectionQuery()
{
$emptySchema = newSchema([
'query' => newObjectType([
'name' => 'QueryRoot',
'fields' => [
'onlyField' => ['type' => stringType()],
],
]),
]);
/** @noinspection PhpUnhandledExceptionInspection */
graphql($emptySchema, $this->introspectionQuery);
$this->addToAssertionCount(1);
}
}
================================================
FILE: tests/Functional/Type/PredicateTest.php
================================================
objectType = newObjectType(['name' => 'Object']);
$this->interfaceType = newInterfaceType(['name' => 'Interface']);
$this->unionType = newUnionType(['name' => 'Union', 'types' => [$this->objectType]]);
$this->enumType = newEnumType(['name' => 'Enum', 'values' => ['foo' => []]]);
$this->inputObjectType = newInputObjectType(['name' => 'InputObject']);
$this->scalarType = newScalarType([
'name' => 'Scalar',
'serialize' => function () {
},
'parseValue' => function () {
},
'parseLiteral' => function () {
},
]);
}
/**
* @throws InvariantException
*/
public function testAssertType()
{
assertType(stringType());
assertType($this->objectType);
$this->addToAssertionCount(2);
}
/**
* @throws InvariantException
*/
public function testAssertScalarTypeWithValidTypes()
{
assertScalarType(stringType());
assertScalarType($this->scalarType);
$this->addToAssertionCount(2);
}
/**
* @expectedException \Exception
*/
public function testAssertScalarTypeWithInvalidTypes()
{
assertScalarType(newList($this->scalarType));
assertScalarType($this->enumType);
}
}
/**
* @param mixed $type
* @throws InvariantException
*/
function assertScalarType($type)
{
if (!($type instanceof ScalarType)) {
throw new InvariantException(\sprintf('Expected %s to be a GraphQL Scalar type.', toString($type)));
}
}
================================================
FILE: tests/Functional/Type/SerializationTest.php
================================================
assertEquals($answer, intType()->serialize($value));
}
/**
* @return array
*/
public function valuesIntCanRepresentDataProvider()
{
return [
[1, 1],
[123, 123],
[0, 0],
[-1, -1],
[100000, 1e5],
];
}
/**
* @param mixed $value
* @dataProvider valuesIntCannotRepresentDataProvider
* @expectedException \Digia\GraphQL\Error\InvalidTypeException
*/
public function testValuesIntCannotRepresent($value)
{
intType()->serialize($value);
$this->addToAssertionCount(1);
}
/**
* @return array
*/
public function valuesIntCannotRepresentDataProvider()
{
return [
[0.1],
[1.1],
[-1.1],
['-1.1'],
[1e100],
[-1e100],
['one'],
];
}
public function testSerializeBooleanToInt()
{
$this->assertEquals(0, intType()->serialize(false));
$this->assertEquals(1, intType()->serialize(true));
}
/**
* @expectedException \@expectedException \Digia\GraphQL\Error\InvalidTypeException
*/
public function testSerializeEmptyStringToInt()
{
intType()->serialize('');
$this->addToAssertionCount(1);
}
/**
* @param mixed $value
* @param mixed $answer
* @dataProvider valuesFloatCanRepresentDataProvider
*/
public function testValuesFloatCanRepresent($value, $answer)
{
$this->assertEquals($answer, floatType()->serialize($value));
}
/**
* @return array
*/
public function valuesFloatCanRepresentDataProvider()
{
return [
[1, 1.0],
[0, 0.0],
['123.5', 123.5],
[-1, -1.0],
[0.1, 0.1],
[1.1, 1.1],
[-1.1, -1.1],
['-1.1', -1.1],
[false, 0.0],
[true, 1.0],
];
}
/**
* @param mixed $value
* @dataProvider valuesFloatCannotRepresentDataProvider
* @expectedException \Digia\GraphQL\Error\InvalidTypeException
*/
public function testValuesFloatCannotRepresent($value)
{
floatType()->serialize($value);
$this->addToAssertionCount(1);
}
/**
* @return array
*/
public function valuesFloatCannotRepresentDataProvider()
{
return [['one'], ['']];
}
/**
* @param mixed $value
* @param mixed $answer
* @dataProvider valuesStringCanRepresentDataProvider
*/
public function testValuesStringCanRepresent($value, $answer)
{
$this->assertEquals($answer, stringType()->serialize($value));
}
/**
* @return array
*/
public function valuesStringCanRepresentDataProvider()
{
return [
['string', 'string'],
[1, '1'],
[-1.1, '-1.1'],
[true, 'true'],
[false, 'false'],
];
}
/**
* @dataProvider valuesBooleanCanRepresentDataProvider
*/
public function testValuesBooleanCanRepresent($value, $answer)
{
$this->assertEquals($answer, booleanType()->serialize($value));
}
/**
* @return array
*/
public function valuesBooleanCanRepresentDataProvider()
{
return [
['string', true],
['', false],
[1, true],
[0, false],
[true, true],
[false, false],
];
}
}
================================================
FILE: tests/Functional/Type/introspection.graphql
================================================
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type { ...TypeRef }
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}
================================================
FILE: tests/Functional/Validation/MessagesTest.php
================================================
assertEquals(undefinedFieldMessage('f', 'T', [], []), 'Cannot query field "f" on type "T".');
}
public function testUndefinedFieldMessageWorksWithNoSmallNumbersOfTypeSuggestions()
{
$this->assertEquals(
undefinedFieldMessage('f', 'T', ['A', 'B'], []),
'Cannot query field "f" on type "T". Did you mean to use an inline fragment on "A" or "B"?'
);
}
public function testUndefinedFieldMessageWorksWithNoSmallNumbersOfFieldSuggestions()
{
$this->assertEquals(
undefinedFieldMessage('f', 'T', [], ['z', 'y']),
'Cannot query field "f" on type "T". Did you mean "z" or "y"?'
);
}
public function testUndefinedFieldMessageOnlyShowsOneSetOfSuggestionsAtATimePreferringTypes()
{
$this->assertEquals(
undefinedFieldMessage('f', 'T', ['A', 'B'], ['z', 'y']),
'Cannot query field "f" on type "T". Did you mean to use an inline fragment on "A" or "B"?'
);
}
public function testUndefinedFieldMessageLimitsLotsOfTypeSuggestions()
{
$this->assertEquals(
undefinedFieldMessage('f', 'T', ['A', 'B', 'C', 'D', 'E', 'F'], []),
'Cannot query field "f" on type "T". Did you mean to use an inline fragment on "A", "B", "C", "D" or "E"?'
);
}
public function testUndefinedFieldMessageLimitsLotsOfFieldSuggestions()
{
$this->assertEquals(
undefinedFieldMessage('f', 'T', [], ['z', 'y', 'x', 'w', 'v', 'u']),
'Cannot query field "f" on type "T". Did you mean "z", "y", "x", "w" or "v"?'
);
}
public function testFieldConflictMessageContainsHintForAliasConflict()
{
$this->assertEquals(
fieldsConflictMessage('x', 'a and b are different fields'),
'Fields "x" conflict because a and b are different fields. Use different aliases on the fields to fetch both if this was intentional.'
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/ExecutableDefinitionsRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
query Foo {
dog {
name
}
}
')
);
}
public function testWithOperationAndFragment()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Foo {
dog {
name
...Frag
}
}
fragment Frag on Dog {
name
}
')
);
}
public function testWithTypeDefinition()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo {
dog {
name
}
}
type Cow {
name: String
}
extend type Dog {
color: String
}
'),
[
nonExecutableDefinition('Cow', [7, 1]),
nonExecutableDefinition('Dog', [11, 1]),
]
);
}
public function testWithSchemaDefinition()
{
$this->expectFailsRule(
$this->rule,
dedent('
schema {
query: Query
}
type Query {
test: String
}
extend schema @directive
'),
[
nonExecutableDefinition('schema', [1, 1]),
nonExecutableDefinition('Query', [5, 1]),
nonExecutableDefinition('schema', [9, 1]),
]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/FieldOnCorrectTypeRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
fragment objectFieldSelection on Dog {
__typename
name
}
')
);
}
public function testAliasedObjectFieldSelection()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment objectFieldSelection on Dog {
otherName : name
}
')
);
}
public function testInterfaceFieldSelection()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment interfaceFieldSelection on Pet {
__typename
name
}
')
);
}
public function testAliasedInterfaceFieldSelection()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment interfaceFieldSelection on Pet {
otherName : name
}
')
);
}
public function testLyingAliasSelection()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment lyingAliasSelection on Dog {
name : nickname
}
')
);
}
public function testIgnoresFieldOnUnknownType()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment unknownSelection on UnknownType {
unknownField
}
')
);
}
public function testReportsErrorWhenTypeIsKnownAgain()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment typeKnownAgain on Pet {
unknown_pet_field {
... on Cat {
unknown_cat_field
}
}
}
'),
[
undefinedField('unknown_pet_field', 'Pet', [], [], [2, 3]),
undefinedField('unknown_cat_field', 'Cat', [], [], [4, 7]),
]
);
}
public function testFieldNotDefinedOnFragment()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment fieldNotDefined on Dog {
meowVolume
}
'),
[undefinedField('meowVolume', 'Dog', [], ['barkVolume'], [2, 3])]
);
}
public function testIgnoresDeeplyUnknownField()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment deepFieldNotDefined on Dog {
unknown_field {
deeper_unknown_field
}
}
'),
[undefinedField('unknown_field', 'Dog', [], [], [2, 3])]
);
}
public function testSubFieldNotDefined()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment subFieldNotDefined on Human {
pets {
unknown_field
}
}
'),
[undefinedField('unknown_field', 'Pet', [], [], [3, 5])]
);
}
public function testFieldNotDefinedOnInlineFragment()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment fieldNotDefined on Pet {
... on Dog {
meowVolume
}
}
'),
[undefinedField('meowVolume', 'Dog', [], ['barkVolume'], [3, 5])]
);
}
public function testAliasedFieldTargetNotDefined()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment aliasedFieldTargetNotDefined on Dog {
volume: mooVolume
}
'),
[undefinedField('mooVolume', 'Dog', [], ['barkVolume'], [2, 3])]
);
}
public function testAliasedLyingFieldTargetNotDefined()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment aliasedLyingFieldTargetNotDefined on Dog {
barkVolume: kawVolume
}
'),
[undefinedField('kawVolume', 'Dog', [], ['barkVolume'], [2, 3])]
);
}
public function testNotDefinedOnInterface()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment notDefinedOnInterface on Pet {
tailLength
}
'),
[undefinedField('tailLength', 'Pet', [], [], [2, 3])]
);
}
public function testDefinedOnImplementorsButNotOnInterface()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment definedOnImplementorsButNotInterface on Pet {
nickname
}
'),
[undefinedField('nickname', 'Pet', ['Dog', 'Cat'], ['name'], [2, 3])]
);
}
public function testMetaFieldSelectionOnUnion()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment directFieldSelectionOnUnion on CatOrDog {
__typename
}
')
);
}
public function testDirectFieldSelectionOnUnion()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment directFieldSelectionOnUnion on CatOrDog {
directField
}
'),
[undefinedField('directField', 'CatOrDog', [], [], [2, 3])]
);
}
public function testDefinedOnImplementorsQueriedOnUnion()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment definedOnImplementorsQueriedOnUnion on CatOrDog {
name
}
'),
[
undefinedField(
'name',
'CatOrDog',
['Being', 'Pet', 'Canine', 'Dog', 'Cat'],
[],
[2, 3]
)
]
);
}
public function testValidFieldInInlineFragment()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment objectFieldSelection on Pet {
... on Dog {
name
}
... {
name
}
}
')
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/FragmentsOnCompositeTypesRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
fragment validFragment on Dog {
barks
}
')
);
}
public function testInterfaceIsValidFragmentType()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment validFragment on Pet {
name
}
')
);
}
public function testObjectIsValidInlineFragmentType()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment validFragment on Pet {
... on Dog {
barks
}
}
')
);
}
public function testInlineFragmentWithoutTypeIsValid()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment validFragment on Pet {
... {
name
}
}
')
);
}
public function testUnionIsValidFragmentType()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment validFragment on CatOrDog {
__typename
}
')
);
}
public function testScalarIsInvalidFragmentType()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment scalarFragment on Boolean {
bad
}
'),
[fragmentOnNonComposite('scalarFragment', 'Boolean', [1, 28])]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/KnownArgumentNamesRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
fragment argOnRequiredArg on Dog {
doesKnownCommand(dogCommand: SIT)
}
')
);
}
public function testMultipleArgumentsAreKnown()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment multipleArgs on ComplicatedArgs {
multipleReqs(req1: 1, req2: 2)
}
')
);
}
public function testIgnoresArgumentsOfUnknownFields()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment argOnUnknownField on Dog {
unknownField(unknownArg: SIT)
}
')
);
}
public function testMultipleArgumentsInReverseOrderAreKnown()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment multipleArgsReverseOrder on ComplicatedArgs {
multipleReqs(req2: 2, req1: 1)
}
')
);
}
public function testNoArgumentsOnOptionalArguments()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment noArgOnOptionalArg on Dog {
isHouseTrained
}
')
);
}
public function testArgumentsAreKnownDeeply()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
dog {
doesKnowCommand(dogCommand: SIT)
}
human {
pet {
... on Dog {
doesKnowCommand(dogCommand: SIT)
}
}
}
}
')
);
}
public function testDirectiveArgumentsAreKnown()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
dog @skip(if: true)
}
')
);
}
public function testUndirectArgumentsAreInvalid()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
dog @skip(unless: true)
}
'),
[unknownDirectiveArgument('unless', 'skip', [], [2, 13])]
);
}
public function testMisspelledDirectiveArgumentsAreReported()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
dog @skip(iff: true)
}
'),
[unknownDirectiveArgument('iff', 'skip', ['if'], [2, 13])]
);
}
public function testInvalidArgumentName()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment invalidArgName on Dog {
doesKnowCommand(unknown: true)
}
'),
[unknownArgument('unknown', 'doesKnowCommand', 'Dog', [], [2, 19])]
);
}
public function testMisspelledArgumentNameIsReported()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment invalidArgName on Dog {
doesKnowCommand(dogcommand: true)
}
'),
[unknownArgument('dogcommand', 'doesKnowCommand', 'Dog', ['dogCommand'], [2, 19])]
);
}
public function testUnknownArgumentsAmongstKnownArguments()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment oneGoodArgOneInvalidArg on Dog {
doesKnowCommand(whoknows: 1, dogCommand: SIT, unknown: true)
}
'),
[
unknownArgument('whoknows', 'doesKnowCommand', 'Dog', [], [2, 19]),
unknownArgument('unknown', 'doesKnowCommand', 'Dog', [], [2, 49]),
]
);
}
public function testUnknownArgumentsDeeply()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
dog {
doesKnowCommand(unknown: true)
}
human {
pet {
... on Dog {
doesKnowCommand(unknown: true)
}
}
}
}
'),
[
unknownArgument('unknown', 'doesKnowCommand', 'Dog', [], [3, 21]),
unknownArgument('unknown', 'doesKnowCommand', 'Dog', [], [8, 25]),
]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/KnownDirectivesRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
query Foo {
name
...Frag
}
fragment Frag on Dog {
name
}
')
);
}
public function testWithKnownDirectives()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
dog @include(if: true) {
name
}
human @skip(if: false) {
name
}
}
')
);
}
public function testWithUnknownDirective()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
dog @unknown(directive: "value") {
name
}
}
'),
[unknownDirective('unknown', [2, 7])]
);
}
public function testWithManyUnknownDirectives()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
dog @unknown(directive: "value") {
name
}
human @unknown(directive: "value") {
name
pets @unknown(directive: "value") {
name
}
}
}
'),
[
unknownDirective('unknown', [2, 7]),
unknownDirective('unknown', [5, 9]),
unknownDirective('unknown', [7, 10]),
]
);
}
public function testWithWellPlacedDirectives()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Foo @onQuery {
name @include(if: true)
...Frag @include(if: true)
skippedField @skip(if: true)
...SkippedFrag @skip(if: true)
}
mutation Bar @onMutation {
someField
}
')
);
}
public function testWithMisplacedDirectives()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo @include(if: true) {
name @onQuery
...Frag @onQuery
}
mutation Bar @onQuery {
someField
}
'),
[
misplacedDirective('include', 'QUERY', [1, 11]),
misplacedDirective('onQuery', 'FIELD', [2, 8]),
misplacedDirective('onQuery', 'FRAGMENT_SPREAD', [3, 11]),
misplacedDirective('onQuery', 'MUTATION', [6, 14]),
]
);
}
public function testWithinSchemaLanguageWithWellPlacedDirectives()
{
$this->expectPassesRule(
$this->rule,
dedent('
type MyObj implements MyInterface @onObject {
myField(myArg: Int @onArgumentDefinition): String @onFieldDefinition
}
extend type MyObj @onObject
scalar MyScalar @onScalar
extend scalar MyScalar @onScalar
interface MyInterface @onInterface {
myField(myArg: Int @onArgumentDefinition): String @onFieldDefinition
}
extend interface MyInterface @onInterface
union MyUnion @onUnion = MyObj | Other
extend union MyUnion @onUnion
enum MyEnum @onEnum {
MY_VALUE @onEnumValue
}
extend enum MyEnum @onEnum
input MyInput @onInputObject {
myField: Int @onInputFieldDefinition
}
extend input MyInput @onInputObject
schema @onSchema {
query: MyQuery
}
extend schema @onSchema
')
);
}
public function testWithinSchemaLanguageWithMisplacedDirectives()
{
$this->expectFailsRule(
$this->rule,
dedent('
type MyObj implements MyInterface @onInterface {
myField(myArg: Int @onInputFieldDefinition): String @onInputFieldDefinition
}
scalar MyScalar @onEnum
interface MyInterface @onObject {
myField(myArg: Int @onInputFieldDefinition): String @onInputFieldDefinition
}
union MyUnion @onEnumValue = MyObj | Other
enum MyEnum @onScalar {
MY_VALUE @onUnion
}
input MyInput @onEnum {
myField: Int @onArgumentDefinition
}
schema @onObject {
query: MyQuery
}
extend schema @onObject
'),
[
misplacedDirective('onInterface', 'OBJECT', [1, 35]),
misplacedDirective(
'onInputFieldDefinition',
'ARGUMENT_DEFINITION',
[2, 22]
),
misplacedDirective(
'onInputFieldDefinition',
'FIELD_DEFINITION',
[2, 55]
),
misplacedDirective('onEnum', 'SCALAR', [5, 17]),
misplacedDirective('onObject', 'INTERFACE', [7, 23]),
misplacedDirective(
'onInputFieldDefinition',
'ARGUMENT_DEFINITION',
[8, 22]
),
misplacedDirective(
'onInputFieldDefinition',
'FIELD_DEFINITION',
[8, 55]
),
misplacedDirective('onEnumValue', 'UNION', [11, 15]),
misplacedDirective('onScalar', 'ENUM', [13, 13]),
misplacedDirective('onUnion', 'ENUM_VALUE', [14, 12]),
misplacedDirective('onEnum', 'INPUT_OBJECT', [17, 15]),
misplacedDirective(
'onArgumentDefinition',
'INPUT_FIELD_DEFINITION',
[18, 16]
),
misplacedDirective('onObject', 'SCHEMA', [21, 8]),
misplacedDirective('onObject', 'SCHEMA', [25, 15]),
]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/KnownFragmentNamesRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
{
human(id: 4) {
...HumanFields1
... on Human {
...HumanFields2
}
... {
name
}
}
}
fragment HumanFields1 on Human {
name
...HumanFields3
}
fragment HumanFields2 on Human {
name
}
fragment HumanFields3 on Human {
name
}
')
);
}
public function testUnknownFragmentNamesAreInvalid()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
human(id: 4) {
...UnknownFragment1
... on Human {
...UnknownFragment2
}
}
}
fragment HumanFields on Human {
name
...UnknownFragment3
}
'),
[
unknownFragment('UnknownFragment1', [3, 8]),
unknownFragment('UnknownFragment2', [5, 10]),
unknownFragment('UnknownFragment3', [11, 6]),
]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/KnownTypeNamesRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
query Foo($var: String, $required: [String!]!) {
user(id: 4) {
pets { ... on Pet { name }, ...PetFields, ... { name } }
}
}
fragment PetFields on Pet {
name
}
')
);
}
public function testUnknownTypeNamesAreInvalid()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo($var: JumbledUpLetters) {
user(id: 4) {
name
pets { ... on Badger { name }, ...PetFields }
}
}
fragment PetFields on Peettt {
name
}
'),
[
unknownType('JumbledUpLetters', [], [1, 17]),
unknownType('Badger', [], [4, 19]),
unknownType('Peettt', ['Pet'], [7, 23]),
]
);
}
public function testIgnoresTypeDefinitions()
{
$this->expectFailsRule(
$this->rule,
dedent('
type NotInTheSchema {
field: FooBar
}
interface FooBar {
field: NotInTheSchema
}
union U = A | B
input Blob {
field: UnknownType
}
query Foo($var: NotInTheSchema) {
user(id: $var) {
id
}
}
'),
[unknownType('NotInTheSchema', [], [11, 17])]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/LoneAnonymousOperationRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
fragment fragA on Type {
field
}
')
);
}
public function testOneAnonymousOperation()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
field
}
')
);
}
public function testMultipleNamedOperations()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Foo {
field
}
query Bar {
field
}
')
);
}
public function testAnonymousOperationWithFragment()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
...Foo
}
fragment Foo on Type {
field
}
')
);
}
public function testMultipleAnonymousOperations()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
fieldA
}
{
fieldB
}
'),
[
anonymousOperationNotAlone([1, 1]),
anonymousOperationNotAlone([4, 1]),
]
);
}
public function testAnonymousOperationWithAMutation()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
fieldA
}
mutation Foo {
fieldB
}
'),
[anonymousOperationNotAlone([1, 1])]
);
}
public function testAnonymousOperationWithASubscription()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
fieldA
}
subscription Foo {
fieldB
}
'),
[anonymousOperationNotAlone([1, 1])]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/NoFragmentCyclesRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
fragment fragA on Dog { ...fragB }
fragment fragB on Dog { name }
')
);
}
public function testSpreadingTwiceIsNotCircular()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment fragA on Dog { ...fragB, ...fragB }
fragment fragB on Dog { name }
')
);
}
public function testSpreadingTwiceIndirectlyIsNotCircular()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment fragA on Dog { ...fragB, ...fragC }
fragment fragB on Dog { ...fragC }
fragment fragC on Dog { name }
')
);
}
public function testDoubleSpreadWithinAbstractTypes()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment nameFragment on Pet {
... on Dog { name }
... on Cat { name }
}
fragment spreadsInAnon on Pet {
... on Dog { ...nameFragment }
... on Cat { ...nameFragment }
}
')
);
}
public function testDoesNotFalsePositiveOnUnknownFragment()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment nameFragment on Pet {
...UnknownFragment
}
')
);
}
public function testSpreadingRecursivelyWithinFieldsFails()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment fragA on Human { relatives { ...fragA } },
'),
[fragmentCycle('fragA', [], [[1, 39]])]
);
}
public function testNoSpreadingItselfDirectly()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment fragA on Dog { ...fragA }
'),
[fragmentCycle('fragA', [], [[1, 25]])]
);
}
public function testNoSpreadingItselfDirectlyWithinInlineFragment()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment fragA on Pet {
... on Dog {
...fragA
}
}
'),
[fragmentCycle('fragA', [], [[3, 5]])]
);
}
public function testNoSpreadingItselfIndirectly()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment fragA on Dog { ...fragB }
fragment fragB on Dog { ...fragA }
'),
[fragmentCycle('fragA', ['fragB'], [[1, 25], [2, 25]])]
);
}
public function testNoSpreadingItselfIndirectlyInOppositeOrder()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment fragB on Dog { ...fragA }
fragment fragA on Dog { ...fragB }
'),
[fragmentCycle('fragB', ['fragA'], [[1, 25], [2, 25]])]
);
}
public function testNoSpreadingItselfIndirectlyWithinInlineFragment()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment fragA on Pet {
... on Dog {
...fragB
}
}
fragment fragB on Pet {
... on Dog {
...fragA
}
}
'),
[fragmentCycle('fragA', ['fragB'], [[3, 5], [8, 5]])]
);
}
public function testNoSpreadingItselfDeeply()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment fragA on Dog { ...fragB }
fragment fragB on Dog { ...fragC }
fragment fragC on Dog { ...fragO }
fragment fragX on Dog { ...fragY }
fragment fragY on Dog { ...fragZ }
fragment fragZ on Dog { ...fragO }
fragment fragO on Dog { ...fragP }
fragment fragP on Dog { ...fragA, ...fragX }
'),
[
fragmentCycle(
'fragA',
['fragB', 'fragC', 'fragO', 'fragP'],
[[1, 25], [2, 25], [3, 25], [7, 25], [8, 25]]
),
fragmentCycle(
'fragO',
['fragP', 'fragX', 'fragY', 'fragZ'],
[[7, 25], [8, 35], [4, 25], [5, 25], [6, 25]]
)
]
);
}
public function testNoSpreadingItselfDeeplyTwoPaths()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment fragA on Dog { ...fragB, ...fragC }
fragment fragB on Dog { ...fragA }
fragment fragC on Dog { ...fragA }
'),
[
fragmentCycle('fragA', ['fragB'], [[1, 25], [2, 25]]),
fragmentCycle('fragA', ['fragC'], [[1, 35], [3, 25]]),
]
);
}
public function testNoSpreadingItselfDeeplyTwoPathsOppositeOrder()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment fragA on Dog { ...fragC }
fragment fragB on Dog { ...fragC }
fragment fragC on Dog { ...fragA, ...fragB }
'),
[
fragmentCycle('fragA', ['fragC'], [[1, 25], [3, 25]]),
fragmentCycle('fragC', ['fragB'], [[3, 35], [2, 25]]),
]
);
}
public function testNoSpreadingItselfDeeplyAndImmediately()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment fragA on Dog { ...fragB }
fragment fragB on Dog { ...fragB, ...fragC }
fragment fragC on Dog { ...fragA, ...fragB }
'),
[
fragmentCycle('fragB', [], [[2, 25]]),
fragmentCycle('fragA', ['fragB', 'fragC'], [[1, 25], [2, 35], [3, 25]]),
fragmentCycle('fragB', ['fragC'], [[2, 35], [3, 35]]),
]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/NoUndefinedVariablesRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
query Foo($a: String, $b: String, $c: String) {
field(a: $a, b: $b, c: $c)
}
')
);
}
public function testAllVariablesDeeplyDefined()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Foo($a: String, $b: String, $c: String) {
field(a: $a) {
field(b: $b) {
field(c: $c)
}
}
}
')
);
}
public function testAllVariablesDeeplyInInlineFragmentsDefined()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Foo($a: String, $b: String, $c: String) {
... on Type {
field(a: $a) {
field(b: $b) {
... on Type {
field(c: $c)
}
}
}
}
}
')
);
}
public function testAllVariablesInFragmentsDeeplyDefined()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Foo($a: String, $b: String, $c: String) {
...FragA
}
fragment FragA on Type {
field(a: $a) {
...FragB
}
}
fragment FragB on Type {
field(b: $b) {
...FragC
}
}
fragment FragC on Type {
field(c: $c)
}
')
);
}
public function testVariablesWithinSingleFragmentDefinedInMultipleOperations()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Foo($a: String) {
...FragA
}
query Bar($a: String) {
...FragA
}
fragment FragA on Type {
field(a: $a)
}
')
);
}
public function testVariableWithinFragmentsDefinedInOperations()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Foo($a: String) {
...FragA
}
query Bar($b: String) {
...FragB
}
fragment FragA on Type {
field(a: $a)
}
fragment FragB on Type {
field(b: $b)
}
')
);
}
public function testVariableWithinRecursiveFragmentDefined()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Foo($a: String) {
...FragA
}
fragment FragA on Type {
field(a: $a) {
...FragA
}
}
')
);
}
public function testVariableNotDefined()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo($a: String, $b: String, $c: String) {
field(a: $a, b: $b, c: $c, d: $d)
}
'),
[undefinedVariable('d', [2, 33], 'Foo', [1, 1])]
);
}
public function testVariableNotDefinedByAnonymousQuery()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
field(a: $a)
}
'),
[undefinedVariable('a', [2, 12], '', [1, 1])]
);
}
public function testMultipleVariablesNotDefined()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo($b: String) {
field(a: $a, b: $b, c: $c)
}
'),
[
undefinedVariable('a', [2, 12], 'Foo', [1, 1]),
undefinedVariable('c', [2, 26], 'Foo', [1, 1]),
]
);
}
public function testVariableInFragmentNotDefinedByAnonymousQuery()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
...FragA
}
fragment FragA on Type {
field(a: $a)
}
'),
[undefinedVariable('a', [5, 12], '', [1, 1])]
);
}
public function testVariableInFragmentNotDefinedByOperation()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo($a: String, $b: String) {
...FragA
}
fragment FragA on Type {
field(a: $a) {
...FragB
}
}
fragment FragB on Type {
field(b: $b) {
...FragC
}
}
fragment FragC on Type {
field(c: $c)
}
'),
[undefinedVariable('c', [15, 12], 'Foo', [1, 1])]
);
}
public function testMultipleVariablesInFragmentsNotDefined()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo($b: String) {
...FragA
}
fragment FragA on Type {
field(a: $a) {
...FragB
}
}
fragment FragB on Type {
field(b: $b) {
...FragC
}
}
fragment FragC on Type {
field(c: $c)
}
'),
[
undefinedVariable('a', [5, 12], 'Foo', [1, 1]),
undefinedVariable('c', [15, 12], 'Foo', [1, 1]),
]
);
}
public function testSingleVariableInFragmentNotDefinedByMultipleOperations()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo($a: String) {
...FragAB
}
query Bar($a: String) {
...FragAB
}
fragment FragAB on Type {
field(a: $a, b: $b)
}
'),
[
undefinedVariable('b', [8, 19], 'Foo', [1, 1]),
undefinedVariable('b', [8, 19], 'Bar', [4, 1]),
]
);
}
public function testVariablesInFragmentNotDefinedByMultipleOperations()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo($b: String) {
...FragAB
}
query Bar($a: String) {
...FragAB
}
fragment FragAB on Type {
field(a: $a, b: $b)
}
'),
[
undefinedVariable('a', [8, 12], 'Foo', [1, 1]),
undefinedVariable('b', [8, 19], 'Bar', [4, 1]),
]
);
}
public function testVariableInFragmentUsedByOtherOperation()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo($b: String) {
...FragA
}
query Bar($a: String) {
...FragB
}
fragment FragA on Type {
field(a: $a)
}
fragment FragB on Type {
field(b: $b)
}
'),
[
undefinedVariable('a', [8, 12], 'Foo', [1, 1]),
undefinedVariable('b', [11, 12], 'Bar', [4, 1]),
]
);
}
public function testMultipleUndefinedVariablesProduceMultipleErrors()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo($b: String) {
...FragAB
}
query Bar($a: String) {
...FragAB
}
fragment FragAB on Type {
field1(a: $a, b: $b)
...FragC
field3(a: $a, b: $b)
}
fragment FragC on Type {
field2(c: $c)
}
'),
[
undefinedVariable('a', [8, 13], 'Foo', [1, 1]),
undefinedVariable('a', [10, 13], 'Foo', [1, 1]),
undefinedVariable('c', [13, 13], 'Foo', [1, 1]),
undefinedVariable('b', [8, 20], 'Bar', [4, 1]),
undefinedVariable('b', [10, 20], 'Bar', [4, 1]),
undefinedVariable('c', [13, 13], 'Bar', [4, 1]),
]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/NoUnusedFragmentsRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
{
human(id: 4) {
...HumanFields1
... on Human {
...HumanFields2
}
}
}
fragment HumanFields1 on Human {
name
...HumanFields3
}
fragment HumanFields2 on Human {
name
}
fragment HumanFields3 on Human {
name
}
')
);
}
public function testContainsUnknownFragments()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo {
human(id: 4) {
...HumanFields1
}
}
query Bar {
human(id: 4) {
...HumanFields2
}
}
fragment HumanFields1 on Human {
name
...HumanFields3
}
fragment HumanFields2 on Human {
name
}
fragment HumanFields3 on Human {
name
}
fragment Unused1 on Human {
name
}
fragment Unused2 on Human {
name
}
'),
[
unusedFragment('Unused1', [21, 1]),
unusedFragment('Unused2', [24, 1]),
]
);
}
public function testContainsUnknownFragmentsWithReferenceCycle()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo {
human(id: 4) {
...HumanFields1
}
}
query Bar {
human(id: 4) {
...HumanFields2
}
}
fragment HumanFields1 on Human {
name
...HumanFields3
}
fragment HumanFields2 on Human {
name
}
fragment HumanFields3 on Human {
name
}
fragment Unused1 on Human {
name
...Unused2
}
fragment Unused2 on Human {
name
...Unused1
}
'),
[
unusedFragment('Unused1', [21, 1]),
unusedFragment('Unused2', [25, 1]),
]
);
}
public function testContainsUnknownAndUndefinedFragments()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo {
human(id: 4) {
...bar
}
}
fragment foo on Human {
name
}
'),
[unusedFragment('foo', [6, 1])]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/NoUnusedVariablesRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
query ($a: String, $b: String, $c: String) {
field(a: $a, b: $b, c: $c)
}
')
);
}
public function testUsesAllVariablesDeeply()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Foo($a: String, $b: String, $c: String) {
field(a: $a) {
field(b: $b) {
field(c: $c)
}
}
}
')
);
}
public function testUsesAllVariablesDeeplyInInlineFragments()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Foo($a: String, $b: String, $c: String) {
... on Type {
field(a: $a) {
field(b: $b) {
... on Type {
field(c: $c)
}
}
}
}
}
')
);
}
public function testUsesAllVariablesInFragments()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Foo($a: String, $b: String, $c: String) {
...FragA
}
fragment FragA on Type {
field(a: $a) {
...FragB
}
}
fragment FragB on Type {
field(b: $b) {
...FragC
}
}
fragment FragC on Type {
field(c: $c)
}
')
);
}
public function testVariableUsedByFragmentInMultipleOperations()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Foo($a: String) {
...FragA
}
query Bar($b: String) {
...FragB
}
fragment FragA on Type {
field(a: $a)
}
fragment FragB on Type {
field(b: $b)
}
')
);
}
public function testVariableUsedByRecursiveFragment()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Foo($a: String) {
...FragA
}
fragment FragA on Type {
field(a: $a) {
...FragA
}
}
')
);
}
public function testVariableNotUsed()
{
$this->expectFailsRule(
$this->rule,
dedent('
query ($a: String, $b: String, $c: String) {
field(a: $a, b: $b)
}
'),
[unusedVariable('c', null, [1, 32])]
);
}
public function testMultipleVariablesNotUsed()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo($a: String, $b: String, $c: String) {
field(b: $b)
}
'),
[
unusedVariable('a', 'Foo', [1, 11]),
unusedVariable('c', 'Foo', [1, 35]),
]
);
}
public function testVariableNotUsedInFragments()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo($a: String, $b: String, $c: String) {
...FragA
}
fragment FragA on Type {
field(a: $a) {
...FragB
}
}
fragment FragB on Type {
field(b: $b) {
...FragC
}
}
fragment FragC on Type {
field
}
'),
[unusedVariable('c', 'Foo', [1, 35])]
);
}
public function testMultipleVariablesNotUsedInFragments()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo($a: String, $b: String, $c: String) {
...FragA
}
fragment FragA on Type {
field {
...FragB
}
}
fragment FragB on Type {
field(b: $b) {
...FragC
}
}
fragment FragC on Type {
field
}
'),
[
unusedVariable('a', 'Foo', [1, 11]),
unusedVariable('c', 'Foo', [1, 35]),
]
);
}
public function testVariableNotUsedByUnreferencedFragment()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo($b: String) {
...FragA
}
fragment FragA on Type {
field(a: $a)
}
fragment FragB on Type {
field(b: $b)
}
'),
[unusedVariable('b', 'Foo', [1, 11])]
);
}
public function testVariableNotUsedByFragmentUsedByOtherOperation()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo($b: String) {
...FragA
}
query Bar($a: String) {
...FragB
}
fragment FragA on Type {
field(a: $a)
}
fragment FragB on Type {
field(b: $b)
}
'),
[
unusedVariable('b', 'Foo', [1, 11]),
unusedVariable('a', 'Bar', [4, 11]),
]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/OverlappingFieldsCanBeMergedRuleTest.php
================================================
someBox = newInterfaceType([
'name' => 'SomeBox',
'fields' => function () {
return [
'deepBox' => ['type' => $this->someBox],
'unrelatedField' => ['type' => stringType()],
];
},
]);
$this->stringBox = newObjectType([
'name' => 'StringBox',
'interfaces' => [$this->someBox],
'fields' => function () {
return [
'scalar' => ['type' => stringType()],
'deepBox' => ['type' => $this->stringBox],
'unrelatedField' => ['type' => stringType()],
'listStringBox' => ['type' => newList($this->stringBox)],
'stringBox' => ['type' => $this->stringBox],
'intBox' => ['type' => $this->intBox],
];
},
]);
$this->intBox = newObjectType([
'name' => 'IntBox',
'interfaces' => [$this->someBox],
'fields' => function () {
return [
'scalar' => ['type' => intType()],
'deepBox' => ['type' => $this->intBox],
'unrelatedField' => ['type' => stringType()],
'listStringBox' => ['type' => newList($this->stringBox)],
'stringBox' => ['type' => $this->stringBox],
'intBox' => ['type' => $this->intBox],
];
},
]);
$this->nonNullStringBox1 = newInterfaceType([
'name' => 'NonNullStringBox1',
'fields' => [
'scalar' => ['type' => newNonNull(stringType())],
],
]);
$this->nonNullStringBox1Impl = newObjectType([
'name' => 'NonNullStringBox1Impl',
'interfaces' => [$this->someBox, $this->nonNullStringBox1],
'fields' => [
'scalar' => ['type' => newNonNull(stringType())],
'unrelatedField' => ['type' => stringType()],
'deepBox' => ['type' => $this->someBox],
],
]);
$this->nonNullStringBox2 = newInterfaceType([
'name' => 'NonNullStringBox2',
'fields' => [
'scalar' => ['type' => newNonNull(stringType())],
],
]);
$this->nonNullStringBox2Impl = newObjectType([
'name' => 'NonNullStringBox2Impl',
'interfaces' => [$this->someBox, $this->nonNullStringBox2],
'fields' => [
'scalar' => ['type' => newNonNull(stringType())],
'unrelatedField' => ['type' => stringType()],
'deepBox' => ['type' => $this->someBox],
],
]);
$this->connection = newObjectType([
'name' => 'Connection',
'fields' => function () {
return [
'edges' => [
'type' => newList(
newObjectType([
'name' => 'Edge',
'fields' => [
'node' => [
'type' => newObjectType([
'name' => 'Node',
'fields' => [
'id' => ['type' => idType()],
'name' => ['type' => stringType()],
],
])
],
],
])
),
],
];
},
]);
$this->schema = newSchema([
'query' => newObjectType([
'name' => 'QueryRoot',
'fields' => function () {
return [
'someBox' => ['type' => $this->someBox],
'connection' => ['type' => $this->connection],
];
},
]),
'types' => [$this->intBox, $this->stringBox, $this->nonNullStringBox1Impl, $this->nonNullStringBox2Impl],
]);
}
public function testUniqueFields()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment uniqueFields on Dog {
name
nickname
}
')
);
}
public function testIdenticalFields()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment mergeIdenticalFields on Dog {
name
name
}
')
);
}
public function testIdenticalFieldsWithIdenticalArguments()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment mergeIdenticalFieldsWithIdenticalArgs on Dog {
doesKnowCommand(dogCommand: SIT)
doesKnowCommand(dogCommand: SIT)
}
')
);
}
public function testIdenticalFieldsWithIdenticalDirectives()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment mergeSameFieldsWithSameDirectives on Dog {
name @include(if: true)
name @include(if: true)
}
')
);
}
public function testDifferentArgumentsWithDifferentAliases()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment differentArgsWithDifferentAliases on Dog {
knowsSit: doesKnowCommand(dogCommand: SIT)
knowsDown: doesKnowCommand(dogCommand: DOWN)
}
')
);
}
public function testDifferentDirectivesWithDifferentAliases()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment differentDirectivesWithDifferentAliases on Dog {
nameIfTrue: name @include(if: true)
nameIfFalse: name @include(if: false)
}
')
);
}
public function testDifferentSkipIncludeDirectivesAccepted()
{
// Note: Differing skip/include directives don't create an ambiguous return
// value and are acceptable in conditions where differing runtime values
// may have the same desired effect of including or skipping a field.
$this->expectPassesRule(
$this->rule,
dedent('
fragment differentDirectivesWithDifferentAliases on Dog {
name @include(if: true)
name @include(if: false)
}
')
);
}
public function testSameAliasesWithDifferentFieldTargets()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment sameAliasesWithDifferentFieldTargets on Dog {
fido: name
fido: nickname
}
'),
[fieldConflict('fido', 'name and nickname are different fields', [[2, 3], [3, 3]])]
);
}
public function testSameAliasesAllowedOnNonOverlappingFields()
{
// This is valid since no object can be both a "Dog" and a "Cat", thus
// these fields can never overlap.
$this->expectPassesRule(
$this->rule,
dedent('
fragment sameAliasesWithDifferentFieldTargets on Pet {
... on Dog {
name
}
... on Cat {
name: nickname
}
}
')
);
}
public function testAliasMaskingDirectFieldAccess()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment aliasMaskingDirectFieldAccess on Dog {
name: nickname
name
}
'),
[fieldConflict('name', 'nickname and name are different fields', [[2, 3], [3, 3]])]
);
}
public function testDifferentArgumentsSecondAddsAnArgument()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment conflictingArgs on Dog {
doesKnowCommand
doesKnowCommand(dogCommand: HEEL)
}
'),
[fieldConflict('doesKnowCommand', 'they have differing arguments', [[2, 3], [3, 3]])]
);
}
public function testDifferentArgsSecondMissingAnArgument()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment conflictingArgs on Dog {
doesKnowCommand(dogCommand: SIT)
doesKnowCommand
}
'),
[fieldConflict('doesKnowCommand', 'they have differing arguments', [[2, 3], [3, 3]])]
);
}
public function testConflictingArguments()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment conflictingArgs on Dog {
doesKnowCommand(dogCommand: SIT)
doesKnowCommand(dogCommand: HEEL)
}
'),
[fieldConflict('doesKnowCommand', 'they have differing arguments', [[2, 3], [3, 3]])]
);
}
public function testAllowsDifferentArgumentsWhereNoConflictIsPossible()
{
// This is valid since no object can be both a "Dog" and a "Cat", thus
// these fields can never overlap.
$this->expectPassesRule(
$this->rule,
dedent('
fragment conflictingArgs on Pet {
... on Dog {
name(surname: true)
}
... on Cat {
name
}
}
')
);
}
public function testEncountersConflictInFragments()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
...A
...B
}
fragment A on Type {
x: a
}
fragment B on Type {
x: b
}
'),
[fieldConflict('x', 'a and b are different fields', [[6, 3], [9, 3]])]
);
}
public function testReportsEachConlictOnce()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
f1 {
...A
...B
}
f2 {
...B
...A
}
f3 {
...A
...B
x: c
}
}
fragment A on Type {
x: a
}
fragment B on Type {
x: b
}
'),
[
fieldConflict('x', 'a and b are different fields', [[17, 3], [20, 3]]),
fieldConflict('x', 'c and a are different fields', [[13, 5], [17, 3]]),
fieldConflict('x', 'c and b are different fields', [[13, 5], [20, 3]]),
]
);
}
public function testDeepConflict()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
field {
x: a
},
field {
x: b
}
}
'),
[
fieldConflict(
'field',
[['x', 'a and b are different fields']],
[[2, 3], [3, 5], [5, 3], [6, 5]]
)
]
);
}
public function testDeepConflictWithMultipleIssues()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
field {
x: a
y: c
},
field {
x: b
y: d
}
}
'),
[
fieldConflict(
'field',
[
['x', 'a and b are different fields'],
['y', 'c and d are different fields'],
],
[[2, 3], [3, 5], [4, 5], [6, 3], [7, 5], [8, 5]]
)
]
);
}
public function testVeryDeepConflict()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
field {
deepField {
x: a
}
},
field {
deepField {
x: b
}
}
}
'),
[
fieldConflict(
'field',
['deepField', [['x', 'a and b are different fields']]],
[[2, 3], [3, 5], [4, 7], [7, 3], [8, 5], [9, 7]]
)
]
);
}
public function testReportsDeepConflictToNearestCommonAncestor()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
field {
deepField {
x: a
}
deepField {
x: b
}
},
field {
deepField {
y
}
}
}
'),
[
fieldConflict(
'deepField',
['x', 'a and b are different fields'],
[[3, 5], [4, 7], [6, 5], [7, 7]]
)
]
);
}
public function testReportsDeepConflictToNearestCommonAncestorInFragments()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
field {
...F
}
field {
...F
}
}
fragment F on T {
deepField {
deeperField {
x: a
}
deeperField {
x: b
}
},
deepField {
deeperField {
y
}
}
}
'),
[
fieldConflict(
'deeperField',
['x', 'a and b are different fields'],
[[11, 5], [12, 7], [14, 5], [15, 7]]
)
]
);
}
public function testReportsDeepConflictsInNestedFragments()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
field {
...F
}
field {
...I
}
}
fragment F on T {
x: a
...G
}
fragment G on T {
y: c
}
fragment I on T {
y: d
...J
}
fragment J on T {
x: b
}
'),
[
fieldConflict(
'field',
[
['x', 'a and b are different fields'],
['y', 'c and d are different fields'],
],
[[2, 3], [10, 3], [14, 3], [5, 3], [21, 3], [17, 3]]
)
]
);
}
public function testIgnoresUnknownFragments()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
field
...Unknown
...Known
}
fragment Known on T {
field
...OtherUnknown
}
')
);
}
public function testConflictingReturnTypesWhichPotentiallyOverlap()
{
// This is invalid since an object could potentially be both the Object
// type IntBox and the interface type NonNullStringBox1. While that
// condition does not exist in the current schema, the schema could
// expand in the future to allow this. Thus it is invalid.
$this->expectFailsRuleWithSchema(
$this->schema,
$this->rule,
dedent('
{
someBox {
...on IntBox {
scalar
}
...on NonNullStringBox1 {
scalar
}
}
}
'),
[
fieldConflict(
'scalar',
'they return conflicting types Int and String!',
[[4, 7], [7, 7]]
)
]
);
}
public function testCompatibleReturnShapesOnDifferentReturnTypes()
{
// In this case `deepBox` returns `SomeBox` in the first usage, and
// `StringBox` in the second usage. These return types are not the same!
// however this is valid because the return *shapes* are compatible.
$this->expectPassesRuleWithSchema(
$this->schema,
$this->rule,
dedent('
{
someBox {
... on SomeBox {
deepBox {
unrelatedField
}
}
... on StringBox {
deepBox {
unrelatedField
}
}
}
}
')
);
}
public function testDisallowsDifferingReturnTypesDespiteNoOverlap()
{
$this->expectFailsRuleWithSchema(
$this->schema,
$this->rule,
dedent('
{
someBox {
... on IntBox {
scalar
}
... on StringBox {
scalar
}
}
}
'),
[
fieldConflict(
'scalar',
'they return conflicting types Int and String',
[[4, 7], [7, 7]]
)
]
);
}
public function testReportsCorrectlyWhenANonExclusiveFollowsAnExclusive()
{
$this->expectFailsRuleWithSchema(
$this->schema,
$this->rule,
dedent('
{
someBox {
... on IntBox {
deepBox {
...X
}
}
}
someBox {
... on StringBox {
deepBox {
...Y
}
}
}
memoed: someBox {
... on IntBox {
deepBox {
...X
}
}
}
memoed: someBox {
... on StringBox {
deepBox {
...Y
}
}
}
other: someBox {
...X
}
other: someBox {
...Y
}
}
fragment X on SomeBox {
scalar
}
fragment Y on SomeBox {
scalar: unrelatedField
}
'),
[
fieldConflict(
'other',
['scalar', 'scalar and unrelatedField are different fields'],
[[30, 3], [38, 3], [33, 3], [41, 3]]
)
]
);
}
public function testDisallowsDifferingReturnTypeNullabilityDespiteNoOverlap()
{
$this->expectFailsRuleWithSchema(
$this->schema,
$this->rule,
'
{
someBox {
... on NonNullStringBox1 {
scalar
}
... on StringBox {
scalar
}
}
}
',
[
fieldConflict(
'scalar',
'they return conflicting types String! and String',
[[4, 7], [7, 7]]
)
]
);
}
public function testDisallowsDifferingReturnTypeListDespiteNoOverlap()
{
$this->expectFailsRuleWithSchema(
$this->schema,
$this->rule,
dedent('
{
someBox {
... on IntBox {
box: listStringBox {
scalar
}
}
... on StringBox {
box: stringBox {
scalar
}
}
}
}
'),
[
fieldConflict(
'box',
'they return conflicting types [StringBox] and StringBox',
[[4, 7], [9, 7]]
)
]
);
$this->expectFailsRuleWithSchema(
$this->schema,
$this->rule,
dedent('
{
someBox {
... on IntBox {
box: stringBox {
scalar
}
}
... on StringBox {
box: listStringBox {
scalar
}
}
}
}
'),
[
fieldConflict(
'box',
'they return conflicting types StringBox and [StringBox]',
[[4, 7], [9, 7]]
)
]
);
}
public function testDisallowsDifferingSubfields()
{
$this->expectFailsRuleWithSchema(
$this->schema,
$this->rule,
dedent('
{
someBox {
... on IntBox {
box: stringBox {
val: scalar
val: unrelatedField
}
}
... on StringBox {
box: stringBox {
val: scalar
}
}
}
}
'),
[
fieldConflict(
'val',
'scalar and unrelatedField are different fields',
[[5, 9], [6, 9]]
)
]
);
}
public function testDisallowsDifferingDeepReturnTypesDespiteNoOverlap()
{
$this->expectFailsRuleWithSchema(
$this->schema,
$this->rule,
dedent('
{
someBox {
... on IntBox {
box: stringBox {
scalar
}
}
... on StringBox {
box: intBox {
scalar
}
}
}
}
'),
[
fieldConflict(
'box',
['scalar', 'they return conflicting types String and Int'],
[[4, 7], [5, 9], [9, 7], [10, 9]]
)
]
);
}
public function testAllowsNonConflictingOverlappingTypes()
{
$this->expectPassesRuleWithSchema(
$this->schema,
$this->rule,
dedent('
{
someBox {
... on IntBox {
scalar: unrelatedField
}
... on StringBox {
scalar
}
}
}
')
);
}
public function testSameWrappedScalarReturnTypes()
{
$this->expectPassesRuleWithSchema(
$this->schema,
$this->rule,
dedent('
{
someBox {
...on NonNullStringBox1 {
scalar
}
...on NonNullStringBox2 {
scalar
}
}
}
')
);
}
public function testAllowsInlineTypelessFragments()
{
$this->expectPassesRuleWithSchema(
$this->schema,
$this->rule,
dedent('
{
a
... {
a
}
}
')
);
}
public function testComparesDeepTypesIncludingList()
{
$this->expectFailsRuleWithSchema(
$this->schema,
$this->rule,
dedent('
{
connection {
...edgeID
edges {
node {
id: name
}
}
}
}
fragment edgeID on Connection {
edges {
node {
id
}
}
}
'),
[
fieldConflict(
'edges',
['node', [['id', 'name and id are different fields']]],
[[4, 5], [5, 7], [6, 9], [13, 3], [14, 5], [15, 7]]
)
]
);
}
public function testIgnoresUnknownTypes()
{
$this->expectPassesRuleWithSchema(
$this->schema,
$this->rule,
dedent('
{
someBox {
...on UnknownType {
scalar
}
...on NonNullStringBox2 {
scalar
}
}
}
')
);
}
public function testDoesNotInfiniteLoopOnRecursiveFragment()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment fragA on Human { name, relatives { name, ...fragA } }
')
);
}
public function testDoesNotInfiniteLoopOnImmediatelyRecursiveFragments()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment fragA on Human { name, ...fragA }
')
);
}
public function testDoesNotInfiniteLoopOnTransitivelyRecursiveFragments()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment fragA on Human { name, ...fragB }
fragment fragB on Human { name, ...fragC }
fragment fragC on Human { name, ...fragA }
')
);
}
public function testFindsInvalidCaseEvenWithImmediatelyRecursiveFragment()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment sameAliasesWithDifferentFieldTargets on Dog {
...sameAliasesWithDifferentFieldTargets
fido: name
fido: nickname
}
'),
[
fieldConflict(
'fido',
'name and nickname are different fields',
[[3, 3], [4, 3]]
)
]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/PossibleFragmentSpreadsRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
fragment objectWithinObject on Dog { ...dogFragment }
fragment dogFragment on Dog { barkVolume }
')
);
}
public function testOfTheSameObjectWithInlineFragment()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment objectWithinObjectAnon on Dog { ... on Dog { barkVolume } }
')
);
}
public function testObjectIntoAnImplementedInterface()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment objectWithinInterface on Pet { ...dogFragment }
fragment dogFragment on Dog { barkVolume }
')
);
}
public function testObjectIntoContainingUnion()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment objectWithinUnion on CatOrDog { ...dogFragment }
fragment dogFragment on Dog { barkVolume }
')
);
}
public function testUnionIntoContainedObject()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment unionWithinObject on Dog { ...catOrDogFragment }
fragment catOrDogFragment on CatOrDog { __typename }
')
);
}
public function testUnionIntoOverlappingInterface()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment unionWithinInterface on Pet { ...catOrDogFragment }
fragment catOrDogFragment on CatOrDog { __typename }
')
);
}
public function testUnionIntoOverlappingUnion()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment unionWithinUnion on DogOrHuman { ...catOrDogFragment }
fragment catOrDogFragment on CatOrDog { __typename }
')
);
}
public function testInterfaceIntoImplementedObject()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment interfaceWithinObject on Dog { ...petFragment }
fragment petFragment on Pet { name }
')
);
}
public function testInterfaceIntoOverlappingInterface()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment interfaceWithinInterface on Pet { ...beingFragment }
fragment beingFragment on Being { name }
')
);
}
public function testInterfaceIntoOverlappingInterfaceInInlineFragment()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment interfaceWithinInterface on Pet { ... on Being { name } }
')
);
}
public function testInterfaceIntoOverlappingUnion()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment interfaceWithinUnion on CatOrDog { ...petFragment }
fragment petFragment on Pet { name }
')
);
}
public function testIgnoresIncorrectType()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment petFragment on Pet { ...badInADifferentWay }
fragment badInADifferentWay on String { name }
')
);
}
public function testDifferentObjectIntoObject()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment invalidObjectWithinObject on Cat { ...dogFragment }
fragment dogFragment on Dog { barkVolume }
'),
[typeIncompatibleSpread('dogFragment', 'Cat', 'Dog', [1, 45])]
);
}
public function testDifferentObjectIntoObjectInInlineFragment()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment invalidObjectWithinObjectAnon on Cat {
... on Dog { barkVolume }
}
'),
[typeIncompatibleAnonymousSpread('Cat', 'Dog', [2, 3])]
);
}
public function testObjectIntoNotImplementingInterface()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment invalidObjectWithinInterface on Pet { ...humanFragment }
fragment humanFragment on Human { pets { name } }
'),
[typeIncompatibleSpread('humanFragment', 'Pet', 'Human', [1, 48])]
);
}
public function testObjectIntoNotContainingUnion()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment invalidObjectWithinUnion on CatOrDog { ...humanFragment }
fragment humanFragment on Human { pets { name } }
'),
[typeIncompatibleSpread('humanFragment', 'CatOrDog', 'Human', [1, 49])]
);
}
public function testUnionIntoNotContainedObject()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment invalidUnionWithinObject on Human { ...catOrDogFragment }
fragment catOrDogFragment on CatOrDog { __typename }
'),
[typeIncompatibleSpread('catOrDogFragment', 'Human', 'CatOrDog', [1, 46])]
);
}
public function testUnionIntoNonOverlappingInterface()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment invalidUnionWithinInterface on Pet { ...humanOrAlienFragment }
fragment humanOrAlienFragment on HumanOrAlien { __typename }
'),
[typeIncompatibleSpread('humanOrAlienFragment', 'Pet', 'HumanOrAlien', [1, 47])]
);
}
public function testUnionIntoNonOverlappingUnion()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment invalidUnionWithinUnion on CatOrDog { ...humanOrAlienFragment }
fragment humanOrAlienFragment on HumanOrAlien { __typename }
'),
[typeIncompatibleSpread('humanOrAlienFragment', 'CatOrDog', 'HumanOrAlien', [1, 48])]
);
}
public function testInterfaceIntoNonImplementingObject()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment invalidInterfaceWithinObject on Cat { ...intelligentFragment }
fragment intelligentFragment on Intelligent { iq }
'),
[typeIncompatibleSpread('intelligentFragment', 'Cat', 'Intelligent', [1, 48])]
);
}
public function testInterfaceIntoNonOverlappingInterface()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment invalidInterfaceWithinInterface on Pet {
...intelligentFragment
}
fragment intelligentFragment on Intelligent { iq }
'),
[typeIncompatibleSpread('intelligentFragment', 'Pet', 'Intelligent', [2, 3])]
);
}
public function testInterfaceIntoNonOverlappingInterfaceInInlineFragment()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment invalidInterfaceWithinInterfaceAnon on Pet {
...on Intelligent { iq }
}
'),
[typeIncompatibleAnonymousSpread('Pet', 'Intelligent', [2, 3])]
);
}
public function testInterfaceIntoNonOverlappingUnion()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment invalidInterfaceWithinUnion on HumanOrAlien { ...petFragment }
fragment petFragment on Pet { name }
'),
[typeIncompatibleSpread('petFragment', 'HumanOrAlien', 'Pet', [1, 56])]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/ProvidedRequiredArgumentsRuleTest.php
================================================
expectPassesRule(
$this->rule,
'
{
dog {
isHouseTrained(unknownArgument: true)
}
}
'
);
}
public function testValidNonNullableValue()
{
$this->expectPassesRule(
$this->rule,
'
{
dog {
isHouseTrained(atOtherHomes: true)
}
}
'
);
}
public function testNoArgumentOnOptionalArgument()
{
$this->expectPassesRule(
$this->rule,
'
{
dog {
isHouseTrained
}
}
'
);
}
public function testNoArgumentOnNonNullFieldWithDefaultValue()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
nonNullFieldWithDefault
}
}
'
);
}
public function testMultipleArguments()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
multipleReqs(req1: 1, req2: 2)
}
}
'
);
}
public function testMultipleArgumentsInReverseOrder()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
multipleReqs(req2: 2, req1: 1)
}
}
'
);
}
public function testNoArgumentsOnMultipleOptional()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
multipleOpts
}
}
'
);
}
public function testOneArgumentOnMultipleOptional()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
multipleOpts(opt1: 1)
}
}
'
);
}
public function testSecondArgumentOnMultipleOptional()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
multipleOpts(opt2: 1)
}
}
'
);
}
public function testMultipleRequiredOnMixedList()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
multipleOptAndReq(req1: 3, req2: 4)
}
}
'
);
}
public function testMultipleRequiredAndOneOptionalOnMixedList()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
multipleOptAndReq(req1: 3, req2: 4, opt1: 5)
}
}
'
);
}
public function testAllRequiredAndOptinalOnMixedList()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
multipleOptAndReq(req1: 3, req2: 4, opt1: 5, opt2: 6)
}
}
'
);
}
public function testMissingOneNonNullableArgument()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
multipleReqs(req2: 2)
}
}
',
[missingFieldArgument('multipleReqs', 'req1', 'Int!', [3, 5])]
);
}
public function testMultipleNonNullableArguments()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
multipleReqs
}
}
',
[
missingFieldArgument('multipleReqs', 'req1', 'Int!', [3, 5]),
missingFieldArgument('multipleReqs', 'req2', 'Int!', [3, 5]),
]
);
}
public function testIncorrectValueAndMissingArgument()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
multipleReqs(req1: "one")
}
}
',
[missingFieldArgument('multipleReqs', 'req2', 'Int!', [3, 5])]
);
}
public function testIgnoresUnknownDirectives()
{
$this->expectPassesRule(
$this->rule,
'
{
dog @unknown
}
'
);
}
public function testDirectivesOfValidTypes()
{
$this->expectPassesRule(
$this->rule,
'
{
dog @include(if: true) {
name
}
human @skip(if: false) {
name
}
}
'
);
}
public function testDirectiveWithMissingTypes()
{
$this->expectFailsRule(
$this->rule,
'
{
dog @include {
name @skip
}
}
',
[
missingDirectiveArgument('include', 'if', 'Boolean!', [2, 7]),
missingDirectiveArgument('skip', 'if', 'Boolean!', [3, 10]),
]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/RuleTestCase.php
================================================
validator = GraphQL::make(ValidatorInterface::class);
$this->rule = GraphQL::make($this->getRuleClassName());
}
protected function expectPassesRule($rule, $query)
{
return $this->expectPassesRuleWithSchema(testSchema(), $rule, $query);
}
protected function expectPassesRuleWithSchema($schema, $rule, $query)
{
return $this->expectValid($schema, [$rule], $query);
}
protected function expectFailsRule($rule, $query, $expectedErrors = [])
{
return $this->expectFailsRuleWithSchema(testSchema(), $rule, $query, $expectedErrors);
}
protected function expectFailsRuleWithSchema($schema, $rule, $query, $expectedErrors = [])
{
return $this->expectInvalid($schema, [$rule], $query, $expectedErrors);
}
protected function expectValid($schema, $rules, $query)
{
/** @noinspection PhpUnhandledExceptionInspection */
$errors = $this->validator->validate($schema, parse(dedent($query)), $rules);
$this->assertEmpty($errors, 'Should validate');
}
protected function expectInvalid($schema, $rules, $query, $expectedErrors): array
{
/** @noinspection PhpUnhandledExceptionInspection */
$errors = $this->validator->validate($schema, parse(dedent($query)), $rules);
$this->assertTrue(count($errors) >= 1, 'Should not validate');
$this->assertEquals($expectedErrors, array_map('Digia\GraphQL\Error\formatError', $errors));
return $errors;
}
}
================================================
FILE: tests/Functional/Validation/Rule/ScalarLeafsRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
fragment scalarSelection on Dog {
barks
}
')
);
}
public function testObjectTypeMissingSelection()
{
$this->expectFailsRule(
$this->rule,
dedent('
query directQueryOnObjectWithoutSubFields {
human
}
'),
[requiredSubselection('human', 'Human', [2, 3])]
);
}
public function testInterfaceTypeMissingSelection()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
human { pets }
}
'),
[requiredSubselection('pets', '[Pet]', [2, 11])]
);
}
public function testValidScalarSelectionWithArguments()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment scalarSelectionWithArgs on Dog {
doesKnowCommand(dogCommand: SIT)
}
')
);
}
public function testScalarSelectionNotAllowedOnBoolean()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment scalarSelectionsNotAllowedOnBoolean on Dog {
barks { sinceWhen }
}
'),
[noSubselectionAllowed('barks', 'Boolean', [2, 9])]
);
}
public function testScalarSelectionNotAllowedOnEnum()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment scalarSelectionsNotAllowedOnEnum on Cat {
furColor { inHexdec }
}
'),
[noSubselectionAllowed('furColor', 'FurColor', [2, 12])]
);
}
public function testScalarSelectionNotAllowedWithArguments()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment scalarSelectionsNotAllowedWithArgs on Dog {
doesKnowCommand(dogCommand: SIT) { sinceWhen }
}
'),
[noSubselectionAllowed('doesKnowCommand', 'Boolean', [2, 36])]
);
}
public function testScalarSelectionNotAllowedWithDirectives()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment scalarSelectionsNotAllowedWithDirectives on Dog {
name @include(if: true) { isAlsoHumanName }
}
'),
[noSubselectionAllowed('name', 'String', [2, 27])]
);
}
public function testScalarSelectionNotAllowedWithDirectivesAndArguments()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment scalarSelectionsNotAllowedWithDirectivesAndArgs on Dog {
doesKnowCommand(dogCommand: SIT) @include(if: true) { sinceWhen }
}
'),
[noSubselectionAllowed('doesKnowCommand', 'Boolean', [2, 55])]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/SingleFieldSubscriptionsRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
subscription ImportantEmails {
importantEmails
}
')
);
}
public function testFailsWithMoreThanOneRootField()
{
$this->expectFailsRule(
$this->rule,
dedent('
subscription ImportantEmails {
importantEmails
notImportantEmails
}
'),
[singleFieldOnly('ImportantEmails', [[3, 3]])]
);
}
public function testFailsWithMoreThanOneRootFieldIncludingIntrospection()
{
$this->expectFailsRule(
$this->rule,
dedent('
subscription ImportantEmails {
importantEmails
__typename
}
'),
[singleFieldOnly('ImportantEmails', [[3, 3]])]
);
}
public function testFailsWithManyMoreThanOneRootField()
{
$this->expectFailsRule(
$this->rule,
dedent('
subscription ImportantEmails {
importantEmails
notImportantEmails
spamEmails
}
'),
[singleFieldOnly('ImportantEmails', [[3, 3], [4, 3]])]
);
}
public function testFailsWithMoreThanOneRootFieldInAnonymousSubscription()
{
$this->expectFailsRule(
$this->rule,
dedent('
subscription {
importantEmails
notImportantEmails
}
'),
[singleFieldOnly(null, [[3, 3]])]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/UniqueArgumentNamesRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
{
field
}
')
);
}
public function testNoArgumentsOnDirective()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
field @directive
}
')
);
}
public function testArgumentOnField()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
field(arg: "value")
}
')
);
}
public function testArgumentOnDirective()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
field @directive(arg: "value")
}
')
);
}
public function testSameArgumentOnTwoFields()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
one: field(arg: "value")
two: field(arg: "value")
}
')
);
}
public function testSameArgumentOnFieldAndDirective()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
field(arg: "value") @directive(arg: "value")
}
')
);
}
public function testSameArgumentOnTwoDirectives()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
field @directive1(arg: "value") @directive2(arg: "value")
}
')
);
}
public function testMultipleFieldArguments()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
field @directive(arg1: "value", arg2: "value", arg3: "value")
}
')
);
}
public function testDuplicateFieldArguments()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
field(arg1: "value", arg1: "value")
}
'),
[duplicateArgument('arg1', [[2, 9], [2, 24]])]
);
}
public function testManyDuplicateFieldArguments()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
field(arg1: "value", arg1: "value", arg1: "value")
}
'),
[
duplicateArgument('arg1', [[2, 9], [2, 24]]),
duplicateArgument('arg1', [[2, 9], [2, 39]]),
]
);
}
public function testDuplicateDirectiveArguments()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
field @directive(arg1: "value", arg1: "value")
}
'),
[duplicateArgument('arg1', [[2, 20], [2, 35]])]
);
}
public function testManyDuplicateDirectiveArguments()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
field @directive(arg1: "value", arg1: "value", arg1: "value")
}
'),
[
duplicateArgument('arg1', [[2, 20], [2, 35]]),
duplicateArgument('arg1', [[2, 20], [2, 50]]),
]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/UniqueDirectivesPerLocationRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
fragment Test on Type {
field
}
')
);
}
public function testUniqueDirectivesInDifferentLocations()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment Test on Type @directiveA {
field @directiveB
}
')
);
}
public function testUniqueDirectivesInSameLocations()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment Test on Type @directiveA @directiveB {
field @directiveA @directiveB
}
')
);
}
public function testSameDirectivesInDifferentLocations()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment Test on Type @directiveA {
field @directiveA
}
')
);
}
public function testSameDirectivesInSimilarLocations()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment Test on Type {
field @directive
field @directive
}
')
);
}
public function testDuplicateDirectivesInOneLocation()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment Test on Type {
field @directive @directive
}
'),
[duplicateDirective('directive', [[2, 9], [2, 20]])]
);
}
public function testManyDuplicateDirectivesInOneLocation()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment Test on Type {
field @directive @directive @directive
}
'),
[
duplicateDirective('directive', [[2, 9], [2, 20]]),
duplicateDirective('directive', [[2, 9], [2, 31]]),
]
);
}
public function testDifferentDuplicateDirectivesInOneLocations()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment Test on Type {
field @directiveA @directiveB @directiveA @directiveB
}
'),
[
duplicateDirective('directiveA', [[2, 9], [2, 33]]),
duplicateDirective('directiveB', [[2, 21], [2, 45]]),
]
);
}
public function testDuplicateDirectivesInManyLocations()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment Test on Type @directive @directive {
field @directive @directive
}
'),
[
duplicateDirective('directive', [[1, 23], [1, 34]]),
duplicateDirective('directive', [[2, 9], [2, 20]]),
]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/UniqueFragmentNamesRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
{
field
}
')
);
}
public function testOneFragment()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
...fragA
}
fragment fragA on Type {
field
}
')
);
}
public function testManyFragments()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
...fragA
...fragB
...fragC
}
fragment fragA on Type {
fieldA
}
fragment fragB on Type {
fieldB
}
fragment fragC on Type {
fieldC
}
')
);
}
public function testInlineFragmentsAreAlwaysUnique()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
...on Type {
fieldA
}
...on Type {
fieldB
}
}
')
);
}
public function testFragmentsNamedTheSame()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
...fragA
}
fragment fragA on Type {
fieldA
}
fragment fragA on Type {
fieldB
}
'),
[duplicateFragment('fragA', [[4, 10], [7, 10]])]
);
}
public function testFragmentsNamedTheSameWithoutBeingReferenced()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment fragA on Type {
fieldA
}
fragment fragA on Type {
fieldB
}
'),
[duplicateFragment('fragA', [[1, 10], [4, 10]])]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/UniqueInputFieldNamesRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
{
field(arg: { f: true })
}
')
);
}
public function testSameInputObjectWithTwoArguments()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
field(arg1: { f: true }, arg2: { f: true })
}
')
);
}
public function testMultipleInputObjectFields()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
field(arg: { f1: "value", f2: "value", f3: "value" })
}
')
);
}
public function testAllowsForNestedInputObjectsWithSimilarFields()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
field(arg: {
deep: {
deep: {
id: 1
}
id: 1
}
id: 1
})
}
')
);
}
public function testDuplicateInputObjectFields()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
field(arg: { f1: "value", f1: "value" })
}
'),
[duplicateInputField('f1', [[2, 16], [2, 29]])]
);
}
public function testManyDuplicateInputObjectFields()
{
$this->expectFailsRule(
$this->rule,
dedent('
{
field(arg: { f1: "value", f1: "value", f1: "value" })
}
'),
[
duplicateInputField('f1', [[2, 16], [2, 29]]),
duplicateInputField('f1', [[2, 16], [2, 42]]),
]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/UniqueOperationNamesRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
fragment fragA on Type {
field
}
')
);
}
public function testOneAnonymousOperation()
{
$this->expectPassesRule(
$this->rule,
dedent('
{
field
}
')
);
}
public function testOneNamedOperation()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Foo {
field
}
')
);
}
public function testMultipleOperations()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Foo {
field
}
query Bar {
field
}
')
);
}
public function testMultipleOperationsOfDifferentTypes()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Foo {
field
}
mutation Bar {
field
}
subscription Baz {
field
}
')
);
}
public function testFragmentAndOperationWithTheSameName()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Foo {
...Foo
}
fragment Foo on Type {
field
}
')
);
}
public function testMultipleOperationWithTheSameName()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo {
fieldA
}
query Foo {
fieldB
}
'),
[duplicateOperation('Foo', [[1, 7], [5, 7]])]
);
}
public function testMultipleOperationsWithTheSameNameOfDifferentTypesMutation()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo {
fieldA
}
mutation Foo {
fieldB
}
'),
[duplicateOperation('Foo', [[1, 7], [5, 10]])]
);
}
public function testMultipleOperationsWithTheSameNameOfDifferentTypesSubscription()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo {
fieldA
}
subscription Foo {
fieldB
}
'),
[duplicateOperation('Foo', [[1, 7], [5, 14]])]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/UniqueVariableNamesRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
query A($x: Int, $y: String) { __typename }
query B($x: String, $y: Int) { __typename }
')
);
}
public function testDuplicateVariableNames()
{
$this->expectFailsRule(
$this->rule,
dedent('
query A($x: Int, $x: Int, $x: String) { __typename }
query B($x: String, $x: Int) { __typename }
query C($x: Int, $x: Int) { __typename }
'),
[
duplicateVariable('x', [[1, 10], [1, 19]]),
duplicateVariable('x', [[1, 10], [1, 28]]),
duplicateVariable('x', [[2, 10], [2, 22]]),
duplicateVariable('x', [[3, 10], [3, 19]]),
]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/ValuesOfCorrectTypeRuleTest.php
================================================
expectPassesRule(
$this->rule,
'
{
complicatedArgs {
intArgField(intArg: 2)
}
}
'
);
}
public function testGoodNegativeIntValue()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
intArgField(intArg: -2)
}
}
'
);
}
public function testGoodBooleanValue()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
booleanArgField(booleanArg: true)
}
}
'
);
}
public function testGoodStringValue()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
stringArgField(stringArg: "foo")
}
}
'
);
}
public function testGoodFloatValue()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
floatArgField(floatArg: 1.1)
}
}
'
);
}
public function testGoodNegativeFloatValue()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
floatArgField(floatArg: -1.1)
}
}
'
);
}
public function testIntIntoFloat()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
floatArgField(floatArg: 1)
}
}
'
);
}
public function testStringIntoID()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
idArgField(idArg: "someIdString")
}
}
'
);
}
public function testGoodEnumValue()
{
$this->expectPassesRule(
$this->rule,
'
{
dog {
doesKnowCommand(dogCommand: SIT)
}
}
'
);
}
public function testEnumWithUndefinedValue()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
enumArgField(enumArg: UNKNOWN)
}
}
'
);
}
public function testEnumWithNullValue()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
enumArgField(enumArg: NO_FUR)
}
}
'
);
}
public function testNullIntoNullableType()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
intArgField(intArg: null)
}
}
'
);
$this->expectPassesRule(
$this->rule,
'
{
dog(a: null, b: null, c:{ requiredField: true, intField: null }) {
name
}
}
'
);
}
public function testIntIntoString()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
stringArgField(stringArg: 1)
}
}
',
[badValue('String', '1', [3, 31])]
);
}
public function testFloatIntoString()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
stringArgField(stringArg: 1.0)
}
}
',
[badValue('String', '1.0', [3, 31])]
);
}
public function testBooleanIntoString()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
stringArgField(stringArg: true)
}
}
',
[badValue('String', 'true', [3, 31])]
);
}
public function testUnquotedStringIntoString()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
stringArgField(stringArg: BAR)
}
}
',
[badValue('String', 'BAR', [3, 31])]
);
}
public function testStringIntoInt()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
intArgField(intArg: "3")
}
}
',
[badValue('Int', '"3"', [3, 25])]
);
}
public function testBigIntIntoInt()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
intArgField(intArg: 829384293849283498239482938)
}
}
',
[badValue('Int', '829384293849283498239482938', [3, 25])]
);
}
public function testUnquotedStringIntoInt()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
intArgField(intArg: FOO)
}
}
',
[badValue('Int', 'FOO', [3, 25])]
);
}
public function testSimpleFloatIntoInt()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
intArgField(intArg: 3.0)
}
}
',
[badValue('Int', '3.0', [3, 25])]
);
}
public function testFloatIntoInt()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
intArgField(intArg: 3.333)
}
}
',
[badValue('Int', '3.333', [3, 25])]
);
}
public function testStringIntoFloat()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
floatArgField(floatArg: "3.333")
}
}
',
[badValue('Float', '"3.333"', [3, 29])]
);
}
public function testBooleanIntoFloat()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
floatArgField(floatArg: true)
}
}
',
[badValue('Float', 'true', [3, 29])]
);
}
public function testUnquotedStringIntoFloat()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
floatArgField(floatArg: FOO)
}
}
',
[badValue('Float', 'FOO', [3, 29])]
);
}
public function testIntIntoBoolean()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
booleanArgField(booleanArg: 2)
}
}
',
[badValue('Boolean', '2', [3, 33])]
);
}
public function testFloatIntoBoolean()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
booleanArgField(booleanArg: 1.0)
}
}
',
[badValue('Boolean', '1.0', [3, 33])]
);
}
public function testStringIntoBoolean()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
booleanArgField(booleanArg: "true")
}
}
',
[badValue('Boolean', '"true"', [3, 33])]
);
}
public function testUnquotedStringIntoBoolean()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
booleanArgField(booleanArg: TRUE)
}
}
',
[badValue('Boolean', 'TRUE', [3, 33])]
);
}
public function testFloatIntoID()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
idArgField(idArg: 1.0)
}
}
',
[badValue('ID', '1.0', [3, 23])]
);
}
public function testBooleanIntoID()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
idArgField(idArg: true)
}
}
',
[badValue('ID', 'true', [3, 23])]
);
}
public function testUnquotedStringIntoID()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
idArgField(idArg: SOMETHING)
}
}
',
[badValue('ID', 'SOMETHING', [3, 23])]
);
}
public function testIntIntoEnum()
{
$this->expectFailsRule(
$this->rule,
'
{
dog {
doesKnowCommand(dogCommand: 2)
}
}
',
[badValue('DogCommand', '2', [3, 33])]
);
}
public function testFloatIntoEnum()
{
$this->expectFailsRule(
$this->rule,
'
{
dog {
doesKnowCommand(dogCommand: 1.0)
}
}
',
[badValue('DogCommand', '1.0', [3, 33])]
);
}
public function testStringIntoEnum()
{
$this->expectFailsRule(
$this->rule,
'
{
dog {
doesKnowCommand(dogCommand: "SIT")
}
}
',
[badValue('DogCommand', '"SIT"', [3, 33], 'Did you mean the enum value SIT?')]
);
}
public function testBooleanIntoEnum()
{
$this->expectFailsRule(
$this->rule,
'
{
dog {
doesKnowCommand(dogCommand: true)
}
}
',
[badValue('DogCommand', 'true', [3, 33])]
);
}
public function testEnumValueIntoEnum()
{
$this->expectFailsRule(
$this->rule,
'
{
dog {
doesKnowCommand(dogCommand: JUGGLE)
}
}
',
[badValue('DogCommand', 'JUGGLE', [3, 33])]
);
}
public function testDifferentCaseEnumValueIntoEnum()
{
$this->expectFailsRule(
$this->rule,
'
{
dog {
doesKnowCommand(dogCommand: sit)
}
}
',
[badValue('DogCommand', 'sit', [3, 33], 'Did you mean the enum value SIT?')]
);
}
public function testGoodListValue()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
stringListArgField(stringListArg: ["one", null, "two"])
}
}
'
);
}
public function testEmptyListValue()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
stringListArgField(stringListArg: [])
}
}
'
);
}
public function testNullValue()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
stringListArgField(stringListArg: null)
}
}
'
);
}
public function testSingleValueIntoList()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
stringListArgField(stringListArg: "one")
}
}
'
);
}
public function testInvalidListValueWithIncorrectItemType()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
stringListArgField(stringListArg: ["one", 2])
}
}
',
[badValue('String', '2', [3, 47])]
);
}
public function testIntValueIntoListOfStrings()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
stringListArgField(stringListArg: 1)
}
}
',
[badValue('[String]', '1', [3, 39])]
);
}
public function testValidNonNullableValueOnOptionalArgument()
{
$this->expectPassesRule(
$this->rule,
'
{
dog {
isHouseTrained(atOtherHomes: true)
}
}
'
);
}
public function testNoValueOnOptionalArgument()
{
$this->expectPassesRule(
$this->rule,
'
{
dog {
isHouseTrained
}
}
'
);
}
public function testMultipleArguments()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
multipleReqs(req1: 1, req2: 2)
}
}
'
);
}
public function testMultipleArgumentsInReverseOrder()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
multipleReqs(req2: 2, req1: 1)
}
}
'
);
}
public function testNoArgumentsOnMultipleOptionalArguments()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
multipleReqs
}
}
'
);
}
public function testNoArgumentOnMultipleOptionalArguments()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
multipleOpts
}
}
'
);
}
public function testOneArgumentOnMultipleOptionalArguments()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
multipleOpts(opt1: 1)
}
}
'
);
}
public function testSecondArgumentOnMultipleOptionalArguments()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
multipleOpts(opt2: 1)
}
}
'
);
}
public function testMultipleRequiredArgumentsOnMixedList()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
multipleOptAndReq(req1: 3, req2: 4)
}
}
'
);
}
public function testAllRequiredAndOptionalArgumentsOnMixedList()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
multipleOptAndReq(req1: 3, req2: 4, opt1: 5, opt2: 6)
}
}
'
);
}
public function testInvalidNonNullableValueWithIncorrectValueType()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
multipleReqs(req2: "two", req1: "one")
}
}
',
[
badValue('Int!', '"two"', [3, 24]),
badValue('Int!', '"one"', [3, 37]),
]
);
}
public function testIncorrectValueAndMissingArgument()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
multipleReqs(req1: "one")
}
}
',
[badValue('Int!', '"one"', [3, 24])]
);
}
public function testInvalidNonNullableValueWithNullValue()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
multipleReqs(req1: null)
}
}
',
[badValue('Int!', 'null', [3, 24])]
);
}
public function testOptionalArgumentsDespiteRequiredFieldInType()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
complexArgField
}
}
'
);
}
public function testPartialObjectOnlyRequired()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
complexArgField(complexArg: { requiredField: true })
}
}
'
);
}
public function testParitalObjectRequiredFieldCanBeFalsey()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
complexArgField(complexArg: { requiredField: false })
}
}
'
);
}
public function testPartialObjectIncludingRequired()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
complexArgField(complexArg: { requiredField: true, intField: 4 })
}
}
'
);
}
public function testFullObject()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
complexArgField(complexArg: {
requiredField: true,
intField: 4,
stringField: "foo",
booleanField: false,
stringListField: ["one", "two"]
})
}
}
'
);
}
public function testFullObjectWithFieldsInDifferentOrder()
{
$this->expectPassesRule(
$this->rule,
'
{
complicatedArgs {
complexArgField(complexArg: {
stringListField: ["one", "two"],
booleanField: false,
requiredField: true,
stringField: "foo",
intField: 4,
})
}
}
'
);
}
public function testPartialObjectWithMissingRequiredArgument()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
complexArgField(complexArg: { intField: 4 })
}
}
',
[requiredField('ComplexInput', 'requiredField', 'Boolean!', [3, 33])]
);
}
public function testParitalObjectWithInvalidFieldType()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
complexArgField(complexArg: {
stringListField: ["one", 2],
requiredField: true,
})
}
}
',
[badValue('String', '2', [4, 32])]
);
}
public function testPartialObjectWithNullToNonNullField()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
complexArgField(complexArg: {
requiredField: true,
nonNullField: null,
})
}
}
',
[badValue('Boolean!', 'null', [5, 21])]
);
}
public function testPartialObjectUnknownFieldArgument()
{
$this->expectFailsRule(
$this->rule,
'
{
complicatedArgs {
complexArgField(complexArg: {
requiredField: true,
unknownField: "value"
})
}
}
',
[unknownField(
'ComplexInput',
'unknownField',
[5, 7],
'Did you mean nonNullField, intField or booleanField?'
)]
);
}
public function testReportsOriginalErrorForCustomScalarWhichThrows()
{
/** @var GraphQLException[] $errors */
$errors = $this->expectFailsRule(
$this->rule,
'
{
invalidArg(arg: 123)
}
',
[badValue('Invalid', '123', [2, 19], 'Invalid scalar is always invalid: 123')]
);
$this->assertEquals($errors[0]->getOriginalErrorMessage(), 'Invalid scalar is always invalid: 123');
}
public function testAllowsCustomScalarToAcceptComplexLiterals()
{
$this->expectPassesRule(
$this->rule,
'
{
test1: anyArg(arg: 123)
test2: anyArg(arg: "abc")
test3: anyArg(arg: [123, "abc"])
test4: anyArg(arg: {deep: [123, "abc"]})
}
'
);
}
public function testDirectiveArgumentsWithDirectivesOfValidTypes()
{
$this->expectPassesRule(
$this->rule,
'
{
dog @include(if: true) {
name
}
human @skip(if: false) {
name
}
}
'
);
}
public function testDirectiveArgumentsWithDirectiveWithIncorrectTypes()
{
$this->expectFailsRule(
$this->rule,
'
{
dog @include(if: "yes") {
name @skip(if: ENUM)
}
}
',
[
badValue('Boolean!', '"yes"', [2, 20]),
badValue('Boolean!', 'ENUM', [3, 20]),
]
);
}
public function testVariablesWithValidDefaultValues()
{
$this->expectPassesRule(
$this->rule,
'
query WithDefaultValues(
$a: Int = 1,
$b: String = "ok",
$c: ComplexInput = { requiredField: true, intField: 3 }
) {
dog { name }
}
'
);
}
public function testVariablesWithValidDefaultNullValues()
{
$this->expectPassesRule(
$this->rule,
'
query WithDefaultValues(
$a: Int = null,
$b: String = null,
$c: ComplexInput = { requiredField: true, intField: null }
) {
dog { name }
}
'
);
}
public function testVariablesWithInvalidDefaultNullValues()
{
$this->expectFailsRule(
$this->rule,
'
query WithDefaultValues(
$a: Int! = null,
$b: String! = null,
$c: ComplexInput = { requiredField: null, intField: null }
) {
dog { name }
}
',
[
badValue('Int!', 'null', [2, 14]),
badValue('String!', 'null', [3, 17]),
badValue('Boolean!', 'null', [4, 39]),
]
);
}
public function testVariablesWithInvalidDefaultValues()
{
$this->expectFailsRule(
$this->rule,
'
query InvalidDefaultValues(
$a: Int = "one",
$b: String = 4,
$c: ComplexInput = "notverycomplex"
) {
dog { name }
}
',
[
badValue('Int', '"one"', [2, 13]),
badValue('String', '4', [3, 16]),
badValue('ComplexInput', '"notverycomplex"', [4, 22]),
]
);
}
public function testVariablesWithComplexInvalidDefaultValues()
{
$this->expectFailsRule(
$this->rule,
'
query WithDefaultValues(
$a: ComplexInput = { requiredField: 123, intField: "abc" }
) {
dog { name }
}
',
[
badValue('Boolean!', '123', [2, 39]),
badValue('Int', '"abc"', [2, 54]),
]
);
}
public function testComplexVariablesMissingRequiredField()
{
$this->expectFailsRule(
$this->rule,
'
query MissingRequiredField($a: ComplexInput = {intField: 3}) {
dog { name }
}
',
[requiredField('ComplexInput', 'requiredField', 'Boolean!', [1, 47])]
);
}
public function testListVariablesWithInvalidItem()
{
$this->expectFailsRule(
$this->rule,
'
query InvalidItem($a: [String] = ["one", 2]) {
dog { name }
}
',
[badValue('String', '2', [1, 42])]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/VariablesAreInputTypesRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
query Foo($a: String, $b: [Boolean!]!, $c: ComplexInput) {
field(a: $a, b: $b, c: $c)
}
')
);
}
public function testOutputTypesAreInvalid()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Foo($a: Dog, $b: [[CatOrDog!]]!, $c: Pet) {
field(a: $a, b: $b, c: $c)
}
'),
[
nonInputTypeOnVariable('a', 'Dog', [1, 15]),
nonInputTypeOnVariable('b', '[[CatOrDog!]]!', [1, 24]),
nonInputTypeOnVariable('c', 'Pet', [1, 44]),
]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/VariablesDefaultValueAllowedRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
query NullableValues($a: Int, $b: String, $c: ComplexInput) {
dog { name }
}
')
);
}
public function testRequiredVariablesWithoutDefaultValues()
{
$this->expectPassesRule(
$this->rule,
dedent('
query RequiredValues($a: Int!, $b: String!) {
dog { name }
}
')
);
}
public function testVariablesWithValidDefaultValues()
{
$this->expectPassesRule(
$this->rule,
dedent('
query WithDefaultValues(
$a: Int = 1,
$b: String = "ok",
$c: ComplexInput = { requiredField: true, intField: 3 }
) {
dog { name }
}
')
);
}
public function testVariablesWithValidNullDefaultValues()
{
$this->expectPassesRule(
$this->rule,
dedent('
query WithDefaultValues(
$a: Int = null,
$b: String = null,
$c: ComplexInput = { requiredField: true, intField: null }
) {
dog { name }
}
')
);
}
public function testNoRequiredVariablesWithDefaultValues()
{
$this->expectFailsRule(
$this->rule,
dedent('
query UnreachableDefaultValues($a: Int! = 3, $b: String! = "default") {
dog { name }
}
'),
[
variableDefaultValueNotAllowed('a', 'Int!', 'Int', [1, 43]),
variableDefaultValueNotAllowed('b', 'String!', 'String', [1, 60]),
]
);
}
public function testVariablesWithInvalidDefaultNullValues()
{
$this->expectFailsRule(
$this->rule,
dedent('
query WithDefaultValues($a: Int! = null, $b: String! = null) {
dog { name }
}
'),
[
variableDefaultValueNotAllowed('a', 'Int!', 'Int', [1, 36]),
variableDefaultValueNotAllowed('b', 'String!', 'String', [1, 56]),
]
);
}
}
================================================
FILE: tests/Functional/Validation/Rule/VariablesInAllowedPositionRuleTest.php
================================================
expectPassesRule(
$this->rule,
dedent('
query Query($booleanArg: Boolean)
{
complicatedArgs {
booleanArgField(booleanArg: $booleanArg)
}
}
')
);
}
public function testBooleanAllowsBooleanWithinFragment()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment booleanArgFrag on ComplicatedArgs {
booleanArgField(booleanArg: $booleanArg)
}
query Query($booleanArg: Boolean) {
complicatedArgs {
...booleanArgFrag
}
}
')
);
$this->expectPassesRule(
$this->rule,
dedent('
query Query($booleanArg: Boolean) {
complicatedArgs {
...booleanArgFrag
}
}
fragment booleanArgFrag on ComplicatedArgs {
booleanArgField(booleanArg: $booleanArg)
}
')
);
}
public function testBooleanAllowsNonNullBoolean()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Query($nonNullBooleanArg: Boolean!) {
complicatedArgs {
booleanArgField(booleanArg: $nonNullBooleanArg)
}
}
')
);
}
public function testNonNullBooleanAllowsBooleanWithinFragment()
{
$this->expectPassesRule(
$this->rule,
dedent('
fragment booleanArgFrag on ComplicatedArgs {
booleanArgField(booleanArg: $nonNullBooleanArg)
}
query Query($nonNullBooleanArg: Boolean!) {
complicatedArgs {
...booleanArgFrag
}
}
')
);
}
public function testNonNullIntAllowsIntWithDefaultValue()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Query($intArg: Int = 1) {
complicatedArgs {
nonNullIntArgField(nonNullIntArg: $intArg)
}
}
')
);
}
public function testListOfStringsAllowsListOfStrings()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Query($stringListVar: [String]) {
complicatedArgs {
stringListArgField(stringListArg: $stringListVar)
}
}
')
);
}
public function testListOfNonNullStringsAllowsListOfStrings()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Query($stringListVar: [String!]) {
complicatedArgs {
stringListArgField(stringListArg: $stringListVar)
}
}
')
);
}
public function testStringAllowsListOfStrings()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Query($stringVar: String) {
complicatedArgs {
stringListArgField(stringListArg: [$stringVar])
}
}
')
);
}
public function testNonNullStringAllowsListOfStrings()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Query($stringVar: String!) {
complicatedArgs {
stringListArgField(stringListArg: [$stringVar])
}
}
')
);
}
public function testComplexInputAllowsComplexInput()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Query($complexVar: ComplexInput) {
complicatedArgs {
complexArgField(complexArg: $complexVar)
}
}
')
);
}
public function testComplexInputAllowsComplexInputInField()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Query($boolVar: Boolean = false) {
complicatedArgs {
complexArgField(complexArg: {requiredArg: $boolVar})
}
}
')
);
}
public function testNonNullBooleanAllowsNonNullBooleanInDirective()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Query($boolVar: Boolean!) {
dog @include(if: $boolVar)
}
')
);
}
public function testBooleanAllowsNonNullBooleanInDirectiveWithDefaultValue()
{
$this->expectPassesRule(
$this->rule,
dedent('
query Query($boolVar: Boolean = false) {
dog @include(if: $boolVar)
}
')
);
}
public function testIntDisallowsNonNullInt()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Query($intArg: Int) {
complicatedArgs {
nonNullIntArgField(nonNullIntArg: $intArg)
}
}
'),
[badVariablePosition('intArg', 'Int', 'Int!', [[1, 13], [3, 39]])]
);
}
public function testIntDisallowsNonNullIntWithinFragment()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment nonNullIntArgFieldFrag on ComplicatedArgs {
nonNullIntArgField(nonNullIntArg: $intArg)
}
query Query($intArg: Int) {
complicatedArgs {
...nonNullIntArgFieldFrag
}
}
'),
[badVariablePosition('intArg', 'Int', 'Int!', [[5, 13], [2, 37]])]
);
}
public function testIntDisallowsNonNullIntWithinNestedFragment()
{
$this->expectFailsRule(
$this->rule,
dedent('
fragment outerFrag on ComplicatedArgs {
...nonNullIntArgFieldFrag
}
fragment nonNullIntArgFieldFrag on ComplicatedArgs {
nonNullIntArgField(nonNullIntArg: $intArg)
}
query Query($intArg: Int) {
complicatedArgs {
...outerFrag
}
}
'),
[badVariablePosition('intArg', 'Int', 'Int!', [[9, 13], [6, 37]])]
);
}
public function testBooleanDisallowsString()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Query($stringVar: String) {
complicatedArgs {
booleanArgField(booleanArg: $stringVar)
}
}
'),
[badVariablePosition('stringVar', 'String', 'Boolean', [[1, 13], [3, 33]])]
);
}
public function testStringDisallowsListOfStrings()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Query($stringVar: String) {
complicatedArgs {
stringListArgField(stringListArg: $stringVar)
}
}
'),
[badVariablePosition('stringVar', 'String', '[String]', [[1, 13], [3, 39]])]
);
}
public function testBooleanDisallowsNonNullBooleanInDirective()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Query($boolVar: Boolean) {
dog @include(if: $boolVar)
}
'),
[badVariablePosition('boolVar', 'Boolean', 'Boolean!', [[1, 13], [2, 20]])]
);
}
public function testStringDisallowsNonNullBooleanInDirective()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Query($stringVar: String) {
dog @include(if: $stringVar)
}
'),
[badVariablePosition('stringVar', 'String', 'Boolean!', [[1, 13], [2, 20]])]
);
}
public function testStringDisallowsListOfNonNullStrings()
{
$this->expectFailsRule(
$this->rule,
dedent('
query Query($stringListVar: [String]) {
complicatedArgs {
stringListNonNullArgField(stringListNonNullArg: $stringListVar)
}
}
'),
[badVariablePosition('stringListVar', '[String]', '[String!]', [[1, 13], [3, 53]])]
);
}
}
================================================
FILE: tests/Functional/Validation/ValidationTest.php
================================================
validator = GraphQL::make(ValidatorInterface::class);
}
public function testValidatesQueries()
{
$errors = $this->validateQuery(
testSchema(),
dedent('
query {
catOrDog {
... on Cat {
furColor
}
... on Dog {
isHouseTrained
}
}
}
')
);
$this->assertEquals([], $errors, 'Should validate');
}
public function testDetectsBadScalarParse()
{
$errors = $this->validateQuery(
testSchema(),
dedent('
query {
invalidArg(arg: "bad value")
}
')
);
$this->assertEquals([
'locations' => [['line' => 2, 'column' => 19]],
'message' =>
'Expected type Invalid, found "bad value"; Invalid scalar is always invalid: bad value',
'path' => null,
], formatError($errors[0]));
}
public function testValidatesUsingACustomTypeInfo()
{
$typeInfo = new TypeInfo(testSchema(), function () {
return null;
});
$errors = $this->validateQuery(
testSchema(),
dedent('
query {
catOrDog {
... on Cat {
furColor
}
... on Dog {
isHouseTrained
}
}
}
'),
SupportedRules::build(),
$typeInfo
);
$errorMessages = \array_map(function (ValidationException $ex) {
return $ex->getMessage();
}, $errors);
$this->assertEquals([
'Cannot query field "catOrDog" on type "QueryRoot". Did you mean "catOrDog"?',
'Cannot query field "furColor" on type "Cat". Did you mean "furColor"?',
'Cannot query field "isHouseTrained" on type "Dog". Did you mean "isHouseTrained"?'
], $errorMessages);
}
/**
* @return GraphQLException[]
*/
protected function validateQuery($schema, $query, $rules = null, $typeInfo = null)
{
return $this->validator->validate($schema, parse($query), $rules, $typeInfo);
}
}
================================================
FILE: tests/Functional/Validation/errors.php
================================================
nonExecutableDefinitionMessage($definitionName),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function undefinedField($field, $type, $suggestedTypes, $suggestsFields, $location)
{
return [
'message' => undefinedFieldMessage($field, $type, $suggestedTypes, $suggestsFields),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function fragmentOnNonComposite($fragmentName, $typeName, $location)
{
return [
'message' => fragmentOnNonCompositeMessage($fragmentName, $typeName),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function unknownArgument($argumentName, $fieldName, $typeName, $suggestedArguments, $location)
{
return [
'message' => unknownArgumentMessage($argumentName, $fieldName, $typeName, $suggestedArguments),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function unknownDirectiveArgument($argumentName, $directiveName, $suggestedArguments, $location)
{
return [
'message' => unknownDirectiveArgumentMessage($argumentName, $directiveName, $suggestedArguments),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function unknownDirective($directiveName, $location)
{
return [
'message' => unknownDirectiveMessage($directiveName),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function misplacedDirective($directiveName, $directiveLocation, $location)
{
return [
'message' => misplacedDirectiveMessage($directiveName, $directiveLocation),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function unknownFragment($fragmentName, $location)
{
return [
'message' => unknownFragmentMessage($fragmentName),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function unknownType($typeName, $suggestedTypes, $location)
{
return [
'message' => unknownTypeMessage($typeName, $suggestedTypes),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function anonymousOperationNotAlone($location)
{
return [
'message' => anonymousOperationNotAloneMessage(),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function fragmentCycle($fragmentName, $spreadNames, array $locations)
{
return [
'message' => fragmentCycleMessage($fragmentName, $spreadNames),
'locations' => locationsShorthandToArray($locations),
'path' => null,
];
}
function undefinedVariable($variableName, $variableLocation, $operationName, $operationLocation)
{
return [
'message' => undefinedVariableMessage($variableName, $operationName),
'locations' => [
locationShorthandToArray($variableLocation),
locationShorthandToArray($operationLocation),
],
'path' => null,
];
}
function unusedFragment($fragmentName, $location)
{
return [
'message' => unusedFragmentMessage($fragmentName),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function unusedVariable($variableName, $operationName, $location)
{
return [
'message' => unusedVariableMessage($variableName, $operationName),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function fieldConflict($responseName, $reason, $locations)
{
return [
'message' => fieldsConflictMessage($responseName, $reason),
'locations' => locationsShorthandToArray($locations),
'path' => null,
];
}
function typeIncompatibleSpread($fragmentName, $parentType, $fragmentType, $location)
{
return [
'message' => typeIncompatibleSpreadMessage($fragmentName, $parentType, $fragmentType),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function typeIncompatibleAnonymousSpread($parentType, $fragmentType, $location)
{
return [
'message' => typeIncompatibleAnonymousSpreadMessage($parentType, $fragmentType),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function missingFieldArgument($fieldName, $argumentName, $typeName, $location)
{
return [
'message' => missingFieldArgumentMessage($fieldName, $argumentName, $typeName),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function missingDirectiveArgument($directiveName, $argumentName, $typeName, $location)
{
return [
'message' => missingDirectiveArgumentMessage($directiveName, $argumentName, $typeName),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function noSubselectionAllowed($fieldName, $typeName, $location)
{
return [
'message' => noSubselectionAllowedMessage($fieldName, $typeName),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function requiredSubselection($fieldName, $typeName, $location)
{
return [
'message' => requiresSubselectionMessage($fieldName, $typeName),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function singleFieldOnly($name, $locations)
{
return [
'message' => singleFieldOnlyMessage($name),
'locations' => locationsShorthandToArray($locations),
'path' => null,
];
}
function duplicateArgument($argumentName, $locations)
{
return [
'message' => duplicateArgumentMessage($argumentName),
'locations' => locationsShorthandToArray($locations),
'path' => null,
];
}
function duplicateDirective($directiveName, $locations)
{
return [
'message' => duplicateDirectiveMessage($directiveName),
'locations' => locationsShorthandToArray($locations),
'path' => null,
];
}
function duplicateFragment($fragmentName, $locations)
{
return [
'message' => duplicateFragmentMessage($fragmentName),
'locations' => locationsShorthandToArray($locations),
'path' => null,
];
}
function duplicateInputField($fieldName, $locations)
{
return [
'message' => duplicateInputFieldMessage($fieldName),
'locations' => locationsShorthandToArray($locations),
'path' => null,
];
}
function duplicateOperation($operationName, $locations)
{
return [
'message' => duplicateOperationMessage($operationName),
'locations' => locationsShorthandToArray($locations),
'path' => null,
];
}
function duplicateVariable($variableName, $locations)
{
return [
'message' => duplicateVariableMessage($variableName),
'locations' => locationsShorthandToArray($locations),
'path' => null,
];
}
function nonInputTypeOnVariable($variableName, $typeName, $location)
{
return [
'message' => nonInputTypeOnVariableMessage($variableName, $typeName),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function variableDefaultValueNotAllowed($variableName, $typeName, $guessedTypeName, $location)
{
return [
'message' => variableDefaultValueNotAllowedMessage($variableName, $typeName, $guessedTypeName),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function badVariablePosition($variableName, $typeName, $expectedTypeName, $locations)
{
return [
'message' => badVariablePositionMessage($variableName, $typeName, $expectedTypeName),
'locations' => locationsShorthandToArray($locations),
'path' => null,
];
}
function badValue($typeName, $value, $location, $message = null)
{
return [
'message' => badValueMessage($typeName, $value, $message),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function requiredField($typeName, $fieldName, $fieldNameType, $location)
{
return [
'message' => requiredFieldMessage($typeName, $fieldName, $fieldNameType),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
function unknownField($typeName, $fieldName, $location, $message = null)
{
return [
'message' => unknownFieldMessage($typeName, $fieldName, $message),
'locations' => [locationShorthandToArray($location)],
'path' => null,
];
}
================================================
FILE: tests/Functional/Validation/harness.php
================================================
'Being',
'fields' => function () {
return [
'name' => [
'type' => stringType(),
'args' => ['surname' => ['type' => booleanType()]],
],
];
},
]);
}
function Pet(): InterfaceType
{
static $instance = null;
return $instance ??
$instance = newInterfaceType([
'name' => 'Pet',
'fields' => function () {
return [
'name' => [
'type' => stringType(),
'args' => ['surname' => ['type' => booleanType()]],
],
];
},
]);
}
function Canine(): InterfaceType
{
static $instance = null;
return $instance ??
$instance = newInterfaceType([
'name' => 'Canine',
'fields' => function () {
return [
'name' => [
'type' => stringType(),
'args' => ['surname' => ['type' => booleanType()]],
],
];
},
]);
}
function DogCommand(): EnumType
{
static $instance = null;
return $instance ??
$instance = newEnumType([
'name' => 'DogCommand',
'values' => [
'SIT' => ['value' => 0],
'HEEL' => ['value' => 1],
'DOWN' => ['value' => 2],
],
]);
}
function Dog(): ObjectType
{
static $instance = null;
return $instance ??
$instance = newObjectType([
'name' => 'Dog',
'fields' => function () {
return [
'name' => [
'type' => stringType(),
'args' => ['surname' => ['type' => booleanType()]],
],
'nickname' => ['type' => stringType()],
'barkVolume' => ['type' => intType()],
'barks' => ['type' => booleanType()],
'doesKnowCommand' => [
'type' => booleanType(),
'args' => [
'dogCommand' => ['type' => DogCommand()],
],
],
'isHouseTrained' => [
'type' => booleanType(),
'args' => [
'atOtherHomes' => [
'type' => booleanType(),
'defaultValue' => true,
],
],
],
'isAtLocation' => [
'type' => booleanType(),
'args' => ['x' => ['type' => intType()], 'y' => ['type' => intType()]],
],
];
},
'interfaces' => [Being(), Pet(), Canine()],
]);
}
function Cat(): ObjectType
{
static $instance = null;
return $instance ??
$instance = newObjectType([
'name' => 'Cat',
'fields' => function () {
return [
'name' => [
'type' => stringType(),
'args' => ['surname' => ['type' => booleanType()]],
],
'nickname' => ['type' => stringType()],
'meows' => ['type' => booleanType()],
'meowVolume' => ['type' => intType()],
'furColor' => ['type' => FurColor()],
];
},
'interfaces' => [Being(), Pet()],
]);
}
function CatOrDog(): UnionType
{
static $instance = null;
return $instance ??
$instance = newUnionType([
'name' => 'CatOrDog',
'types' => [Dog(), Cat()],
]);
}
function Intelligent(): InterfaceType
{
static $instance = null;
return $instance ??
$instance = newInterfaceType([
'name' => 'Intelligent',
'fields' => [
'iq' => ['type' => intType()],
],
]);
}
function Human(): ObjectType
{
static $instance = null;
return $instance ??
$instance = newObjectType([
'name' => 'Human',
'interfaces' => [Being(), Intelligent()],
'fields' => function () {
return [
'name' => [
'type' => stringType(),
'args' => ['surname' => ['type' => booleanType()]],
],
'pets' => ['type' => newList(Pet())],
'relatives' => ['type' => newList(Human())],
'iq' => ['type' => intType()],
];
},
]);
}
function Alien(): ObjectType
{
static $instance = null;
return $instance ??
$instance = newObjectType([
'name' => 'Alien',
'interfaces' => [Being(), Intelligent()],
'fields' => function () {
return [
'iq' => ['type' => intType()],
'name' => [
'type' => stringType(),
'args' => ['surname' => ['type' => booleanType()]],
],
'numEyes' => ['type' => intType()],
];
},
]);
}
function DogOrHuman(): UnionType
{
static $instance = null;
return $instance ??
$instance = newUnionType([
'name' => 'DogOrHuman',
'types' => [Dog(), Human()],
]);
}
function HumanOrAlien(): UnionType
{
static $instance = null;
return $instance ??
$instance = newUnionType([
'name' => 'HumanOrAlien',
'types' => [Human(), Alien()],
]);
}
function FurColor(): EnumType
{
static $instance = null;
return $instance ??
$instance = newEnumType([
'name' => 'FurColor',
'values' => [
'BROWN' => ['value' => 0],
'BLACK' => ['value' => 1],
'TAN' => ['value' => 2],
'SPOTTED' => ['value' => 3],
'NO_FUR' => ['value' => 4],
'UNKNOWN' => ['value' => 5],
],
]);
}
function ComplexInput(): InputObjectType
{
static $instance = null;
return $instance ??
$instance = newInputObjectType([
'name' => 'ComplexInput',
'fields' => [
'requiredField' => ['type' => newNonNull(booleanType())],
'nonNullField' => ['type' => newNonNull(booleanType()), 'defaultValue' => false],
'intField' => ['type' => intType()],
'stringField' => ['type' => stringType()],
'booleanField' => ['type' => booleanType()],
'stringListField' => ['type' => newList(stringType())],
],
]);
}
function ComplicatedArgs(): ObjectType
{
static $instance = null;
return $instance ??
$instance = newObjectType([
'name' => 'ComplicatedArgs',
// TODO List
// TODO Coercion
// TODO NotNulls
'fields' => function () {
return [
'intArgField' => [
'type' => stringType(),
'args' => ['intArg' => ['type' => intType()]],
],
'nonNullIntArgField' => [
'type' => stringType(),
'args' => ['nonNullIntArg' => ['type' => newNonNull(intType())]],
],
'stringArgField' => [
'type' => stringType(),
'args' => ['stringArg' => ['type' => stringType()]],
],
'booleanArgField' => [
'type' => stringType(),
'args' => ['booleanArg' => ['type' => booleanType()]],
],
'enumArgField' => [
'type' => stringType(),
'args' => ['enumArg' => ['type' => FurColor()]],
],
'floatArgField' => [
'type' => stringType(),
'args' => ['floatArg' => ['type' => floatType()]],
],
'idArgField' => [
'type' => stringType(),
'args' => ['idArg' => ['type' => idType()]],
],
'stringListArgField' => [
'type' => stringType(),
'args' => ['stringListArg' => ['type' => newList(stringType())]],
],
'stringListNonNullArgField' => [
'type' => stringType(),
'args' => ['stringListNonNullArg' => ['type' => newList(newNonNull(stringType()))]],
],
'complexArgField' => [
'type' => stringType(),
'args' => ['complexArg' => ['type' => ComplexInput()]],
],
'multipleReqs' => [
'type' => stringType(),
'args' => [
'req1' => ['type' => newNonNull(intType())],
'req2' => ['type' => newNonNull(intType())],
],
],
'nonNullFieldWithDefault' => [
'type' => stringType(),
'args' => [
'arg' => ['type' => newNonNull(intType()), 'defaultValue' => 0],
],
],
'multipleOpts' => [
'type' => stringType(),
'args' => [
'opt1' => ['type' => intType(), 'defaultValue' => 0],
'opt2' => ['type' => intType(), 'defaultValue' => 0],
],
],
'multipleOptsAndReq' => [
'type' => stringType(),
'args' => [
'req1' => ['type' => newNonNull(intType())],
'req2' => ['type' => newNonNull(intType())],
'opt1' => ['type' => intType(), 'defaultValue' => 0],
'opt2' => ['type' => intType(), 'defaultValue' => 0],
],
],
];
},
]);
}
function InvalidScalar(): ScalarType
{
static $instance = null;
return $instance ??
$instance = newScalarType([
'name' => 'Invalid',
'serialize' => function ($value) {
return $value;
},
'parseLiteral' => function ($node) {
throw new \Exception(sprintf('Invalid scalar is always invalid: %s', $node->getValue()));
},
'parseValue' => function ($value) {
throw new \Exception(sprintf('Invalid scalar is always invalid: %s', $value));
},
]);
}
function AnyScalar(): ScalarType
{
static $instance = null;
return $instance ??
$instance = newScalarType([
'name' => 'Any',
'serialize' => function ($value) {
return $value;
},
'parseLiteral' => function ($node) {
return $node;
},
'parseValue' => function ($value) {
return $value;
},
]);
}
function QueryRoot(): ObjectType
{
static $instance = null;
return $instance ??
$instance = newObjectType([
'name' => 'QueryRoot',
'fields' => function () {
return [
'human' => [
'args' => ['id' => ['type' => idType()]],
'type' => Human(),
],
'alien' => ['type' => Alien()],
'dog' => ['type' => Dog()],
'cat' => ['type' => Cat()],
'pet' => ['type' => Pet()],
'catOrDog' => ['type' => CatOrDog()],
'dogOrHuman' => ['type' => DogOrHuman()],
'humanOrAlien' => ['type' => HumanOrAlien()],
'complicatedArgs' => ['type' => ComplicatedArgs()],
'invalidArg' => [
'args' => ['arg' => ['type' => InvalidScalar()]],
'type' => stringType(),
],
'anyArg' => [
'args' => ['arg' => ['type' => AnyScalar()]],
'type' => stringType(),
],
];
},
]);
}
/**
* @return Schema
*/
function testSchema(): Schema
{
return newSchema([
'query' => QueryRoot(),
'types' => [Cat(), Dog(), Human(), Alien()],
'directives' => [
IncludeDirective(),
SkipDirective(),
newDirective([
'name' => 'onQuery',
'locations' => ['QUERY'],
]),
newDirective([
'name' => 'onMutation',
'locations' => ['MUTATION'],
]),
newDirective([
'name' => 'onSubscription',
'locations' => ['SUBSCRIPTION'],
]),
newDirective([
'name' => 'onField',
'locations' => ['FIELD'],
]),
newDirective([
'name' => 'onFragmentDefinition',
'locations' => ['FRAGMENT_DEFINITION'],
]),
newDirective([
'name' => 'onFragmentSpread',
'locations' => ['FRAGMENT_SPREAD'],
]),
newDirective([
'name' => 'onInlineFragment',
'locations' => ['INLINE_FRAGMENT'],
]),
newDirective([
'name' => 'onSchema',
'locations' => ['SCHEMA'],
]),
newDirective([
'name' => 'onScalar',
'locations' => ['SCALAR'],
]),
newDirective([
'name' => 'onObject',
'locations' => ['OBJECT'],
]),
newDirective([
'name' => 'onFieldDefinition',
'locations' => ['FIELD_DEFINITION'],
]),
newDirective([
'name' => 'onArgumentDefinition',
'locations' => ['ARGUMENT_DEFINITION'],
]),
newDirective([
'name' => 'onInterface',
'locations' => ['INTERFACE'],
]),
newDirective([
'name' => 'onUnion',
'locations' => ['UNION'],
]),
newDirective([
'name' => 'onEnum',
'locations' => ['ENUM'],
]),
newDirective([
'name' => 'onEnumValue',
'locations' => ['ENUM_VALUE'],
]),
newDirective([
'name' => 'onInputObject',
'locations' => ['INPUT_OBJECT'],
]),
newDirective([
'name' => 'onInputFieldDefinition',
'locations' => ['INPUT_FIELD_DEFINITION'],
]),
],
]);
}
================================================
FILE: tests/Functional/ValidationTest.php
================================================
assertEmpty($this->validateQuery($query));
}
// Notes that non-existent fields are invalid
public function testNodesThatNonExistentFieldsAreInvalid()
{
$query = '
query HeroSpaceshipQuery {
hero {
favoriteSpaceship
}
}
';
$this->assertNotEmpty($this->validateQuery($query));
}
// Requires fields on objects
public function testRequiresFieldsOnObjects()
{
$query = '
query HeroNoFieldsQuery {
hero
}
';
$this->assertNotEmpty($this->validateQuery($query));
}
// Disallows fields on scalars
public function testDisallowsFieldsOnScalars()
{
$query = '
query HeroNoFieldsQuery {
hero {
name {
firstCharacterOfName
}
}
}
';
$this->assertNotEmpty($this->validateQuery($query));
}
// Disallows object fields on interfaces
public function testDisallowsObjectFieldsOnInterfaces()
{
$query = '
query DroidFieldOnCharacter {
hero {
name
primaryFunction
}
}
';
$this->assertNotEmpty($this->validateQuery($query));
}
// Allows object fields in fragments
public function testAllowsObjectFieldsInFragments()
{
$query = '
query DroidFieldInFragment {
hero {
name
...DroidFields
}
}
fragment DroidFields on Droid {
primaryFunction
}
';
$this->assertEmpty($this->validateQuery($query));
}
// Allows object fields in inline fragments
public function testAllowsObjectFieldsInInlineFragments()
{
$query = '
query DroidFieldInFragment {
hero {
name
... on Droid {
primaryFunction
}
}
}
';
$this->assertEmpty($this->validateQuery($query));
}
protected function validateQuery($query)
{
/** @noinspection PhpUnhandledExceptionInspection */
$source = new Source($query, 'StarWars.graphql');
/** @noinspection PhpUnhandledExceptionInspection */
return validate(starWarsSchema(), parse($source));
}
}
================================================
FILE: tests/Functional/starWars.graphqls
================================================
schema {
query: Query
}
type Query {
hero(
"If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode."
episode: Episode
): Character
human(
"id of the human"
id: String!
): Human
droid(
"id of the droid"
id: String!
): Droid
}
"A character in the Star Wars Trilogy"
interface Character {
"The id of the character."
id: String!
"The name of the character."
name: String
"The friends of the character, or an empty list if they have none."
friends: [Character]
"Which movies they appear in."
appearsIn: [Episode]
"All secrets about their past."
secretBackstory: String
}
"A humanoid creature in the Star Wars universe."
type Human implements Character {
"The id of the human."
id: String!
"The name of the human."
name: String
"The friends of the human, or an empty list if they have none."
friends: [Character]
"Which movies they appear in."
appearsIn: [Episode]
"The home planet of the human, or null if unknown."
homePlanet: String
"Where are they from and how they came to be who they are."
secretBackstory: String
}
"A mechanical creature in the Star Wars universe."
type Droid implements Character {
"The id of the droid."
id: String!
"The name of the droid."
name: String
"The friends of the droid, or an empty list if they have none."
friends: [Character]
"Which movies they appear in."
appearsIn: [Episode]
"Construction date and the name of the designer."
secretBackstory: String
"The primary function of the droid."
primaryFunction: String
}
enum Episode {
"Released in 1977."
NEWHOPE,
"Released in 1980."
EMPIRE,
"Released in 1983."
JEDI
}
================================================
FILE: tests/Functional/starWarsData.php
================================================
'Human',
'id' => '1000',
'name' => 'Luke Skywalker',
'friends' => ['1002', '1003', '2000', '2001'],
'appearsIn' => ['NEWHOPE', 'EMPIRE', 'JEDI'],
'homePlanet' => 'Tatooine',
];
}
function vader()
{
return [
'type' => 'Human',
'id' => '1001',
'name' => 'Darth Vader',
'friends' => ['1004'],
'appearsIn' => ['NEWHOPE', 'EMPIRE', 'JEDI'],
'homePlanet' => 'Tatooine',
];
}
function han()
{
return [
'type' => 'Human',
'id' => '1002',
'name' => 'Han Solo',
'friends' => ['1000', '1003', '2001'],
'appearsIn' => ['NEWHOPE', 'EMPIRE', 'JEDI'],
];
}
function leia()
{
return [
'type' => 'Human',
'id' => '1003',
'name' => 'Leia Organa',
'friends' => ['1000', '1002', '2000', '2001'],
'appearsIn' => ['NEWHOPE', 'EMPIRE', 'JEDI'],
'homePlanet' => 'Alderaan',
];
}
function tarkin()
{
return [
'type' => 'Human',
'id' => '1004',
'name' => 'Wilhuff Tarkin',
'friends' => ['1001'],
'appearsIn' => ['NEWHOPE'],
];
}
function humanData()
{
return [
'1000' => luke(),
'1001' => vader(),
'1002' => han(),
'1003' => leia(),
'1004' => tarkin(),
];
}
function threepio()
{
return [
'type' => 'Droid',
'id' => '2000',
'name' => 'C-3PO',
'friends' => ['1000', '1002', '1003', '2001'],
'appearsIn' => [4, 5, 6],
'primaryFunction' => 'Protocol',
];
}
function artoo()
{
return [
'type' => 'Droid',
'id' => '2001',
'name' => 'R2-D2',
'friends' => ['1000', '1002', '1003'],
'appearsIn' => [4, 5, 6],
'primaryFunction' => 'Astromech',
];
}
function droidData()
{
return [
'2000' => threepio(),
'2001' => artoo(),
];
}
function getCharacter($id)
{
return getHuman($id) ?? getDroid($id) ?? null;
}
function getFriends($character)
{
return array_map(function ($id) {
return getCharacter($id);
}, $character['friends']);
}
function getHero($episode)
{
if ($episode === 'EMPIRE') {
return luke();
}
return artoo();
}
function getHuman($id)
{
return humanData()[$id] ?? null;
}
function getDroid($id)
{
return droidData()[$id] ?? null;
}
================================================
FILE: tests/Functional/starWarsSchema.php
================================================
[
'hero' => function ($value, $arguments) {
return getHero($arguments['episode'] ?? null);
},
'human' => function ($value, $arguments) {
return getHuman($arguments['id']);
},
'droid' => function ($value, $arguments) {
return getDroid($arguments['id']);
},
],
'Human' => [
'friends' => function ($human) {
return getFriends($human);
},
'secretBackstory' => function () {
throw new \Exception('secretBackstory is secret.');
},
],
'Droid' => [
'friends' => function ($droid) {
return getFriends($droid);
},
'secretBackstory' => function () {
throw new \Exception('secretBackstory is secret.');
},
],
'Character' => [
'__resolveType' => function ($character) {
return $character['type'];
},
],
]);
}
================================================
FILE: tests/TestCase.php
================================================
assertEquals($expected, $result->toArray());
}
}
================================================
FILE: tests/Unit/Error/GraphQLExceptionTest.php
================================================
assertTrue($exception->hasLocations());
/** @noinspection PhpUnhandledExceptionInspection */
$exception = new GraphQLException(
'This is an exception.',
null,
null,
null,
['some', 'path']
);
$this->assertFalse($exception->hasLocations());
}
public function testToArray()
{
/** @noinspection PhpUnhandledExceptionInspection */
$exception = new GraphQLException(
'This is an exception.',
null,
new Source('qeury { hello }'),
[0, 5],
['some', 'path']
);
$this->assertEquals([
'message' => 'This is an exception.',
'locations' => [
['line' => 1, 'column' => 1],
['line' => 1, 'column' => 6],
],
'path' => ['some', 'path'],
], $exception->toArray());
}
public function testToArrayWithExtensions()
{
/** @noinspection PhpUnhandledExceptionInspection */
$exception = new GraphQLException(
'This is an exception.',
null,
new Source('qeury { hello }'),
[0, 5],
['some', 'path'],
['code' => 'SOME_ERROR_CODE']
);
$this->assertEquals([
'message' => 'This is an exception.',
'locations' => [
['line' => 1, 'column' => 1],
['line' => 1, 'column' => 6],
],
'path' => ['some', 'path'],
'extensions' => ['code' => 'SOME_ERROR_CODE'],
], $exception->toArray());
}
}
================================================
FILE: tests/Unit/Error/Handler/ErrorHandlerTest.php
================================================
mockContext();
$errorHandler->handleExecutionError($exception, $context);
$this->assertTrue($middleware->wasInvoked());
}
public function testMiddleware()
{
$result = [];
$middlewareA = new LoggerMiddleware(function () use (&$result) {
$result[] = 'Middleware A invoked';
});
$middlewareB = new LoggerMiddleware(function () use (&$result) {
$result[] = 'Middleware B invoked';
});
$middlewareC = new LoggerMiddleware(function () use (&$result) {
$result[] = 'Middleware C invoked';
});
$noopMiddleware = new NoopMiddleware();
$errorHandler = new ErrorHandler([$middlewareA, $middlewareB, $noopMiddleware, $middlewareC]);
$exception = new ExecutionException('This is an exception.');
$context = $this->mockContext();
$errorHandler->handleExecutionError($exception, $context);
$this->assertSame([
'Middleware A invoked',
'Middleware B invoked',
], $result);
}
/**
* @return ExecutionContext|\PHPUnit_Framework_MockObject_MockObject
*/
private function mockContext()
{
return $this->getMockBuilder(ExecutionContext::class)
->disableOriginalConstructor()
->getMock();
}
}
class WasInvokedMiddleware extends AbstractErrorMiddleware
{
private $wasInvoked = false;
public function handleExecutionError(ExecutionException $exception, ExecutionContext $context, callable $next)
{
$this->wasInvoked = true;
return $next($exception, $context);
}
public function wasInvoked(): bool
{
return $this->wasInvoked;
}
}
class LoggerMiddleware extends AbstractErrorMiddleware
{
private $logCallback;
public function __construct($logCallback)
{
$this->logCallback = $logCallback;
}
public function handleExecutionError(ExecutionException $exception, ExecutionContext $context, callable $next)
{
\call_user_func($this->logCallback, $exception, $context);
return $next($exception, $context);
}
}
class NoopMiddleware extends AbstractErrorMiddleware
{
}
================================================
FILE: tests/Unit/Execution/ExecutionResultTest.php
================================================
assertNull($executionResult->getData());
$this->assertEmpty($executionResult->getErrors());
$this->assertSame(['data' => null], $executionResult->toArray());
// Start with data and no errors
$executionResult = new ExecutionResult(['foo' => 'bar'], []);
$this->assertEquals(['foo' => 'bar'], $executionResult->getData());
$this->assertEmpty($executionResult->getErrors());
$this->assertSame(['data' => ['foo' => 'bar']], $executionResult->toArray());
// Start with data and an error
$executionResult = new ExecutionResult(['foo' => 'bar'], [new GraphQLException('Error')]);
$this->assertEquals(['foo' => 'bar'], $executionResult->getData());
$this->assertCount(1, $executionResult->getErrors());
$this->assertSame([
'errors' => [
[
'message' => 'Error',
'locations' => null,
]
],
'data' => ['foo' => 'bar'],
], $executionResult->toArray());
// Add another error
$executionResult->addError(new GraphQLException('Another error'));
$this->assertCount(2, $executionResult->getErrors());
$this->assertSame([
'errors' => [
[
'message' => 'Error',
'locations' => null
],
[
'message' => 'Another error',
'locations' => null
]
],
'data' => ['foo' => 'bar'],
], $executionResult->toArray());
}
}
================================================
FILE: tests/Unit/Language/BlockStringValueTest.php
================================================
assertBlockStringEquals($expectedBlockStringLines, $rawStringLines);
}
public function testRemovesEmptyLeadingAndTrailingLines(): void
{
$rawStringLines = [
'',
'',
' Hello',
' World!',
'',
' Yours,',
' GraphQL.',
'',
'',
];
$expectedBlockStringLines = [
'Hello',
' World!',
'',
'Yours,',
' GraphQL.'
];
$this->assertBlockStringEquals($expectedBlockStringLines, $rawStringLines);
}
public function testRemovesBlankLeadingAndTrailingLines(): void
{
$rawStringLines = [
' ',
' ',
' Hello',
' World!',
'',
' Yours,',
' GraphQL.',
' ',
' ',
];
$expectedBlockStringLines = [
'Hello',
' World!',
'',
'Yours,',
' GraphQL.'
];
$this->assertBlockStringEquals($expectedBlockStringLines, $rawStringLines);
}
public function testRetainsIndentationFromFirstLine(): void
{
$rawStringLines = [
' Hello',
' World!',
'',
' Yours,',
' GraphQL.',
' ',
' ',
];
$expectedBlockStringLines = [
' Hello',
' World!',
'',
'Yours,',
' GraphQL.'
];
$this->assertBlockStringEquals($expectedBlockStringLines, $rawStringLines);
}
public function testDoesNotAlterTrailingSpaces(): void
{
$rawStringLines = [
' ',
' Hello, ',
' World! ',
' ',
' Yours, ',
' GraphQL. ',
' ',
];
$expectedBlockStringLines = [
'Hello, ',
' World! ',
' ',
'Yours, ',
' GraphQL. ',
];
$this->assertBlockStringEquals($expectedBlockStringLines, $rawStringLines);
}
/**
* @param array $expectedBlockStringLines
* @param array $rawStringLines
*/
private function assertBlockStringEquals(array $expectedBlockStringLines, array $rawStringLines): void
{
$actualBlockString = blockStringValue(implode("\n", $rawStringLines));
$this->assertSame(implode("\n", $expectedBlockStringLines), $actualBlockString);
}
}
================================================
FILE: tests/Unit/Language/LexerTest.php
================================================
assertSyntaxError("\u{0007}", 'Cannot contain the invalid character "\u0007".', [1, 1]);
}
// accepts BOM header
public function testAcceptsBomCharacter(): void
{
$this->assertLexerTokenPropertiesEqual("\u{FEFF} foo", TokenKindEnum::NAME, [2, 5], 'foo');
}
// records line and column
public function testRecordsLineAndNumber(): void
{
$token = $this->getLexer("\n \r\n \r foo\n")->advance();
$this->assertEquals(TokenKindEnum::NAME, $token->getKind());
$this->assertEquals(8, $token->getStart());
$this->assertEquals(11, $token->getEnd());
$this->assertEquals(4, $token->getLine());
$this->assertEquals(3, $token->getColumn());
$this->assertEquals('foo', $token->getValue());
}
// can be JSON.stringified or util.inspected
public function testCanBeJsonSerialized(): void
{
$token = $this->getLexer('foo')->advance();
$this->assertJsonStringEqualsJsonString(\json_encode([
'kind' => 'Name',
'value' => 'foo',
'line' => 1,
'column' => 1,
]), $token->toJSON());
}
// skips whitespace and comments
public function testSkipsWhitespaceAndComments(): void
{
$whitespaceString = <<assertLexerTokenPropertiesEqual($whitespaceString, TokenKindEnum::NAME, [5, 8], 'foo');
$this->assertLexerTokenPropertiesEqual($commentedString, TokenKindEnum::NAME, [9, 12], 'foo');
$this->assertLexerTokenPropertiesEqual(',,,foo,,,', TokenKindEnum::NAME, [3, 6], 'foo');
}
// errors respect whitespace
public function testErrorsRespectWhitespace(): void
{
$whitespaceErrorString = <<getLexer($whitespaceErrorString)->advance();
} catch (SyntaxErrorException $e) {
$this->assertEquals([['line' => 3, 'column' => 5]], $e->getLocationsAsArray());
}
}
// updates line numbers in error for file context
public function testUpdatesLineNumbersInErrorForFileContext()
{
$caughtError = null;
try {
/** @noinspection PhpUnhandledExceptionInspection */
$source = new Source("\n\n ?\n\n", 'foo.php', new SourceLocation(11, 12));
$this->getLexer($source)->advance();
} catch (SyntaxErrorException $e) {
$caughtError = $e;
}
$this->assertEquals(
"Syntax Error: Cannot parse the unexpected character \"?\".\n" .
"\n" .
"foo.php (13:6)\n" .
"12: \n" .
"13: ?\n" .
" ^\n" .
"14: \n",
(string)$caughtError
);
}
// updates column numbers in error for file context
public function testUpdatesColumnNumbersInErrorForFileContext()
{
$caughtError = null;
try {
/** @noinspection PhpUnhandledExceptionInspection */
$source = new Source('?', 'foo.php', new SourceLocation(1, 5));
$this->getLexer($source)->advance();
} catch (SyntaxErrorException $e) {
$caughtError = $e;
}
$this->assertEquals(
"Syntax Error: Cannot parse the unexpected character \"?\".\n" .
"\n" .
"foo.php (1:5)\n" .
"1: ?\n" .
" ^\n",
(string)$caughtError
);
}
// lexes strings
public function testLexesStrings(): void
{
$this->assertLexerTokenPropertiesEqual('"simple"', TokenKindEnum::STRING, [0, 8], 'simple');
$this->assertLexerTokenPropertiesEqual('" white space "', TokenKindEnum::STRING, [0, 15], ' white space ');
$this->assertLexerTokenPropertiesEqual('"quote \\""', TokenKindEnum::STRING, [0, 10], 'quote "');
$this->assertLexerTokenPropertiesEqual(
'"escaped \\n\\r\\b\\t\\f"',
TokenKindEnum::STRING,
[0, 20],
'escaped \n\r\b\t\f'
);
$this->assertLexerTokenPropertiesEqual('"slashes \\\\ \\/"', TokenKindEnum::STRING, [0, 15], 'slashes \\ /');
$this->assertLexerTokenPropertiesEqual(
'"unicode \\u1234\\u5678\\u90AB\\uCDEF"',
TokenKindEnum::STRING,
[0, 34],
'unicode \u1234\u5678\u90AB\uCDEF'
);
}
// lex reports useful string errors
public function testLexReportsUsefulStringErrors()
{
$this->assertSyntaxError('"', 'Unterminated string.', [1, 2]);
$this->assertSyntaxError('"no end quote', 'Unterminated string.', [1, 14]);
$this->assertSyntaxError(
"'single quotes'",
'Unexpected single quote character (\'), did you mean to use a double quote (")?',
[1, 1]
);
$this->assertSyntaxError(
"\"contains unescaped \u{0007} control char\"",
'Invalid character within String: "\\u0007".',
[1, 21]
);
$this->assertSyntaxError(
"\"null-byte is not \u{0000} end of file\"",
'Invalid character within String: .',
[1, 19]
);
$this->assertSyntaxError("\"multi\nline\"", 'Unterminated string.', [1, 7]);
$this->assertSyntaxError("\"multi\rline\"", 'Unterminated string.', [1, 7]);
$this->assertSyntaxError(
"\"bad \\z esc\"",
'Invalid character escape sequence: \\z.',
[1, 7]
);
$this->assertSyntaxError(
"\"bad \\x esc\"",
'Invalid character escape sequence: \\x.',
[1, 7]
);
$this->assertSyntaxError(
"\"bad \\u1 esc\"",
'Invalid character escape sequence: \\u1 es.',
[1, 7]
);
$this->assertSyntaxError(
"\"bad \\u0XX1 esc\"",
'Invalid character escape sequence: \\u0XX1.',
[1, 7]
);
$this->assertSyntaxError(
"\"bad \\uXXXX esc\"",
'Invalid character escape sequence: \\uXXXX.',
[1, 7]
);
$this->assertSyntaxError(
"\"bad \\uFXXX esc\"",
'Invalid character escape sequence: \\uFXXX.',
[1, 7]
);
$this->assertSyntaxError(
"\"bad \\uXXXF esc\"",
'Invalid character escape sequence: \\uXXXF.',
[1, 7]
);
}
// lexes block strings
public function testLexesBlockStrings(): void
{
$this->assertLexerTokenPropertiesEqual(
'"""simple"""',
TokenKindEnum::BLOCK_STRING,
[0, 12],
'simple'
);
$this->assertLexerTokenPropertiesEqual(
'""" white space """',
TokenKindEnum::BLOCK_STRING,
[0, 19],
' white space '
);
$this->assertLexerTokenPropertiesEqual(
'"""contains " quote"""',
TokenKindEnum::BLOCK_STRING,
[0, 22],
'contains " quote'
);
$this->assertLexerTokenPropertiesEqual(
'"""contains \\""" triplequote"""',
TokenKindEnum::BLOCK_STRING,
[0, 31],
'contains """ triplequote'
);
$this->assertLexerTokenPropertiesEqual(
'"""' . "multi\nline" . '""""',
TokenKindEnum::BLOCK_STRING,
[0, 16],
"multi\nline"
);
$this->assertLexerTokenPropertiesEqual(
'"""' . "multi\rline\r\nnormalized" . '""""',
TokenKindEnum::BLOCK_STRING,
[0, 28],
"multi\nline\nnormalized"
);
$this->assertLexerTokenPropertiesEqual(
'"""' . "unescaped \\n\\r\\b\\t\\f\\u1234" . '""""',
TokenKindEnum::BLOCK_STRING,
[0, 32],
'unescaped \\n\\r\\b\\t\\f\\u1234'
);
$this->assertLexerTokenPropertiesEqual(
'"""' . "slashes \\\\ \\/" . '""""',
TokenKindEnum::BLOCK_STRING,
[0, 19],
"slashes \\\\ \\/"
);
$this->assertLexerTokenPropertiesEqual(
dedent('
"""
spans
multiple
lines
"""
'),
TokenKindEnum::BLOCK_STRING,
[0, 40],
"spans\n multiple\n lines"
);
}
// lex reports useful block string errors
public function testLexReportsUsefulBlockStringErrors()
{
$this->assertSyntaxError('"""', 'Unterminated string.', [1, 4]);
$this->assertSyntaxError('"""no end quote', 'Unterminated string.', [1, 16]);
$this->assertSyntaxError(
"\"\"\"contains unescaped \u{0007} control char\"\"\"",
'Invalid character within String: "\\u0007".',
[1, 23]
);
$this->assertSyntaxError(
"\"\"\"null-byte is not \u{0000} end of file\"\"\"",
'Invalid character within String: .',
[1, 21]
);
}
// lexes numbers
public function testLexesNumbers()
{
$this->assertLexerTokenPropertiesEqual('4', TokenKindEnum::INT, [0, 1], '4');
$this->assertLexerTokenPropertiesEqual('4.123', TokenKindEnum::FLOAT, [0, 5], '4.123');
$this->assertLexerTokenPropertiesEqual('-4', TokenKindEnum::INT, [0, 2], '-4');
$this->assertLexerTokenPropertiesEqual('9', TokenKindEnum::INT, [0, 1], '9');
$this->assertLexerTokenPropertiesEqual('0', TokenKindEnum::INT, [0, 1], '0');
$this->assertLexerTokenPropertiesEqual('-4.123', TokenKindEnum::FLOAT, [0, 6], '-4.123');
$this->assertLexerTokenPropertiesEqual('0.123', TokenKindEnum::FLOAT, [0, 5], '0.123');
$this->assertLexerTokenPropertiesEqual('123e4', TokenKindEnum::FLOAT, [0, 5], '123e4');
$this->assertLexerTokenPropertiesEqual('123E4', TokenKindEnum::FLOAT, [0, 5], '123E4');
$this->assertLexerTokenPropertiesEqual('123e-4', TokenKindEnum::FLOAT, [0, 6], '123e-4');
$this->assertLexerTokenPropertiesEqual('123E-4', TokenKindEnum::FLOAT, [0, 6], '123E-4');
$this->assertLexerTokenPropertiesEqual('123e+4', TokenKindEnum::FLOAT, [0, 6], '123e+4');
$this->assertLexerTokenPropertiesEqual('-1.123e4', TokenKindEnum::FLOAT, [0, 8], '-1.123e4');
$this->assertLexerTokenPropertiesEqual('-1.123E4', TokenKindEnum::FLOAT, [0, 8], '-1.123E4');
$this->assertLexerTokenPropertiesEqual('-1.123e+4', TokenKindEnum::FLOAT, [0, 9], '-1.123e+4');
$this->assertLexerTokenPropertiesEqual('-1.123e4567', TokenKindEnum::FLOAT, [0, 11], '-1.123e4567');
}
// lex reports useful number errors
public function testLexReportsUsefulNumberErrors()
{
$this->assertSyntaxError('00', 'Invalid number, unexpected digit after 0: "0".', [1, 2]);
$this->assertSyntaxError('+1', 'Cannot parse the unexpected character "+".', [1, 1]);
$this->assertSyntaxError('1.', 'Invalid number, expected digit but got: .', [1, 3]);
$this->assertSyntaxError('1.e1', 'Invalid number, expected digit but got: "e".', [1, 3]);
$this->assertSyntaxError('.123', 'Cannot parse the unexpected character ".".', [1, 1]);
$this->assertSyntaxError('1.A', 'Invalid number, expected digit but got: "A".', [1, 3]);
$this->assertSyntaxError('-A', 'Invalid number, expected digit but got: "A".', [1, 2]);
$this->assertSyntaxError('1.0e', 'Invalid number, expected digit but got: .', [1, 5]);
$this->assertSyntaxError('1.0eA', 'Invalid number, expected digit but got: "A".', [1, 5]);
}
// lexes punctuation
public function testLexesPunctuation()
{
$this->assertLexerTokenPropertiesEqual('!', TokenKindEnum::BANG, [0, 1], null);
$this->assertLexerTokenPropertiesEqual('$', TokenKindEnum::DOLLAR, [0, 1], null);
$this->assertLexerTokenPropertiesEqual('(', TokenKindEnum::PAREN_L, [0, 1], null);
$this->assertLexerTokenPropertiesEqual(')', TokenKindEnum::PAREN_R, [0, 1], null);
$this->assertLexerTokenPropertiesEqual('...', TokenKindEnum::SPREAD, [0, 3], null);
$this->assertLexerTokenPropertiesEqual(':', TokenKindEnum::COLON, [0, 1], null);
$this->assertLexerTokenPropertiesEqual('=', TokenKindEnum::EQUALS, [0, 1], null);
$this->assertLexerTokenPropertiesEqual('@', TokenKindEnum::AT, [0, 1], null);
$this->assertLexerTokenPropertiesEqual('[', TokenKindEnum::BRACKET_L, [0, 1], null);
$this->assertLexerTokenPropertiesEqual(']', TokenKindEnum::BRACKET_R, [0, 1], null);
$this->assertLexerTokenPropertiesEqual('{', TokenKindEnum::BRACE_L, [0, 1], null);
$this->assertLexerTokenPropertiesEqual('|', TokenKindEnum::PIPE, [0, 1], null);
$this->assertLexerTokenPropertiesEqual('}', TokenKindEnum::BRACE_R, [0, 1], null);
}
// lex reports useful unknown character error
public function testLexReportsUsefulUnknownCharacterError()
{
$this->assertSyntaxError('..', 'Cannot parse the unexpected character ".".', [1, 1]);
$this->assertSyntaxError('?', 'Cannot parse the unexpected character "?".', [1, 1]);
$this->assertSyntaxError("\u{203B}", 'Cannot parse the unexpected character "\\u203b".', [1, 1]);
$this->assertSyntaxError("\u{200b}", 'Cannot parse the unexpected character "\\u200b".', [1, 1]);
}
// lex reports useful information for dashes in names
public function testLexReportsUsefulInformationForDashesInNames()
{
$lexer = $this->getLexer('a-b');
$firstToken = $lexer->advance();
$this->assertTokenPropertiesEqual($firstToken, TokenKindEnum::NAME, [0, 1], 'a');
$caughtError = null;
try {
$lexer->advance();
} catch (SyntaxErrorException $e) {
$caughtError = $e;
}
$this->assertEquals('Syntax Error: Invalid number, expected digit but got: "b".', $caughtError->getMessage());
$this->assertEquals([['line' => 1, 'column' => 3]], $caughtError->getLocationsAsArray());
}
// produces double linked list of tokens, including comments
public function testProducesDoubleLinkedListOfTokensIncludingComments()
{
$lexer = $this->getLexer(dedent('
{
#comment
field
}
'));
$startToken = $lexer->getToken();
$endToken = null;
do {
$endToken = $lexer->advance();
// Lexer advances over ignored comment tokens to make writing parsers
// easier, but will include them in the linked list result.
$this->assertNotEquals(TokenKindEnum::COMMENT, $endToken->getKind());
} while ($endToken->getKind() !== TokenKindEnum::EOF);
$this->assertNull($startToken->getPrev());
$this->assertNull($endToken->getNext());
$tokens = [];
for ($token = $startToken; null !== $token; $token = $token->getNext()) {
if (!empty($tokens)) {
// Tokens are double-linked, prev should point to last seen token.
$this->assertEquals($tokens[\count($tokens) - 1], $token->getPrev());
}
$tokens[] = $token;
}
$this->assertEquals([
'',
'{',
'Comment',
'Name',
'}',
'',
], \array_map(function (Token $token) {
return $token->getKind();
}, $tokens));
}
/**
* @param string $source
* @param string $expectedExceptionMessage
* @param array $position
*/
private function assertSyntaxError(string $source, string $expectedExceptionMessage, array $position): void
{
try {
$this->getLexer($source)->advance();
$this->fail('Expected an exception to be thrown');
} catch (SyntaxErrorException $e) {
$this->assertEquals('Syntax Error: ' . $expectedExceptionMessage, $e->getMessage());
$this->assertEquals([['line' => $position[0], 'column' => $position[1]]], $e->getLocationsAsArray());
}
}
/**
* @param string $source
* @param string $kind
* @param array $location
* @param mixed $value
*/
private function assertLexerTokenPropertiesEqual(string $source, string $kind, array $location, $value): void
{
$token = $this->getLexer($source)->advance();
$this->assertTokenPropertiesEqual($token, $kind, $location, $value);
}
/**
* @param Token $token
* @param string $kind
* @param array $location
* @param mixed $value
*/
private function assertTokenPropertiesEqual(Token $token, string $kind, array $location, $value): void
{
$this->assertEquals($kind, $token->getKind());
$this->assertEquals($location[0], $token->getStart());
$this->assertEquals($location[1], $token->getEnd());
$this->assertEquals($value, $token->getValue());
}
/**
* @param string|Source $source
* @param array $options
* @return LexerInterface
* @throws \Digia\GraphQL\Error\InvariantException
*/
private function getLexer($source, array $options = []): LexerInterface
{
/** @noinspection PhpUnhandledExceptionInspection */
return new Lexer($source instanceof Source ? $source : new Source($source), $options);
}
}
================================================
FILE: tests/Unit/Language/NodeBuilderTest.php
================================================
builder = GraphQL::make(NodeBuilderInterface::class);
}
public function testBuildArgumentNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::ARGUMENT,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someArgument',
],
'value' => [
'kind' => NodeKindEnum::STRING,
'value' => 'someValue',
],
]);
$this->assertInstanceOf(ArgumentNode::class, $node);
}
public function testBuildBooleanValueNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::BOOLEAN,
'value' => true,
]);
$this->assertInstanceOf(BooleanValueNode::class, $node);
}
public function testBuildDirectiveNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::DIRECTIVE,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someDirective',
],
]);
$this->assertInstanceOf(DirectiveNode::class, $node);
}
public function testBuildDocumentNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::DOCUMENT,
'definitions' => [
'query' => [
'kind' => NodeKindEnum::OBJECT,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'Query',
],
'fields' => [
[
'kind' => NodeKindEnum::FIELD,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'queryField',
],
'type' => [
'kind' => NodeKindEnum::NAMED_TYPE,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'String',
],
],
],
],
],
],
]);
$this->assertInstanceOf(DocumentNode::class, $node);
}
public function testBuildEnumValueNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::ENUM,
'value' => [
'kind' => NodeKindEnum::STRING,
'value' => 'someValue',
],
]);
$this->assertInstanceOf(EnumValueNode::class, $node);
}
public function testBuildEnumTypeDefinitionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::ENUM_TYPE_DEFINITION,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someEnum',
],
'values' => [
[
'kind' => NodeKindEnum::ENUM,
'value' => [
'kind' => NodeKindEnum::STRING,
'value' => 'someValue',
],
],
[
'kind' => NodeKindEnum::ENUM,
'value' => [
'kind' => NodeKindEnum::STRING,
'value' => 'someOtherValue',
],
],
],
]);
$this->assertInstanceOf(EnumTypeDefinitionNode::class, $node);
}
public function testBuildEnumTypeExtensionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::ENUM_TYPE_EXTENSION,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someEnum',
],
'values' => [
[
'kind' => NodeKindEnum::ENUM,
'value' => [
'kind' => NodeKindEnum::STRING,
'value' => 'someOtherValue',
],
],
],
]);
$this->assertInstanceOf(EnumTypeExtensionNode::class, $node);
}
public function testBuildFieldDefinitionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::FIELD_DEFINITION,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someField',
],
'type' => [
'kind' => NodeKindEnum::NAMED_TYPE,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'String',
],
],
]);
$this->assertInstanceOf(FieldDefinitionNode::class, $node);
}
public function testBuildFieldNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::FIELD,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someField',
],
'type' => [
'kind' => NodeKindEnum::NAMED_TYPE,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'String',
],
],
]);
$this->assertInstanceOf(FieldNode::class, $node);
}
public function testBuildFloatValueNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::FLOAT,
'value' => 0.42,
]);
$this->assertInstanceOf(FloatValueNode::class, $node);
}
public function testBuildFragmentDefinitionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::FRAGMENT_DEFINITION,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someFragment',
],
]);
$this->assertInstanceOf(FragmentDefinitionNode::class, $node);
}
public function testBuildFragmentSpreadNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::FRAGMENT_SPREAD,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someFragment',
],
]);
$this->assertInstanceOf(FragmentSpreadNode::class, $node);
}
public function testBuildInlineFragmentNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::INLINE_FRAGMENT,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someFragment',
],
]);
$this->assertInstanceOf(InlineFragmentNode::class, $node);
}
public function testBuildInputObjectTypeDefinitionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::INPUT_OBJECT_TYPE_DEFINITION,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someInputObject',
],
]);
$this->assertInstanceOf(InputObjectTypeDefinitionNode::class, $node);
}
public function testBuildInputObjectTypeExtensionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::INPUT_OBJECT_TYPE_EXTENSION,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someOtherInputObject',
],
]);
$this->assertInstanceOf(InputObjectTypeExtensionNode::class, $node);
}
public function testBuildInputValueDefinitionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::INPUT_VALUE_DEFINITION,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someInputValue',
],
'type' => [
'kind' => NodeKindEnum::NAMED_TYPE,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'String',
],
],
]);
$this->assertInstanceOf(InputValueDefinitionNode::class, $node);
}
public function testBuildInterfaceTypeDefinitionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::INTERFACE_TYPE_DEFINITION,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someInterface',
],
]);
$this->assertInstanceOf(InterfaceTypeDefinitionNode::class, $node);
}
public function testBuildInterfaceTypeExtensionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::INTERFACE_TYPE_EXTENSION,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someOtherInterface',
],
]);
$this->assertInstanceOf(InterfaceTypeExtensionNode::class, $node);
}
public function testBuildIntValueNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::INT,
'value' => 42,
]);
$this->assertInstanceOf(IntValueNode::class, $node);
}
public function testBuildListTypeNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::LIST_TYPE,
'type' => [
'kind' => NodeKindEnum::NAMED_TYPE,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'String',
],
],
]);
$this->assertInstanceOf(ListTypeNode::class, $node);
}
public function testBuildNamedTypeNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::NAMED_TYPE,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'String',
],
]);
$this->assertInstanceOf(NamedTypeNode::class, $node);
}
public function testBuildNameNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::NAME,
'value' => 'SomeName',
]);
$this->assertInstanceOf(NameNode::class, $node);
}
public function testBuildNullValueNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::NULL,
]);
$this->assertInstanceOf(NullValueNode::class, $node);
}
public function testBuildObjectFieldNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::OBJECT_FIELD,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someObjectField',
],
'value' => [
'kind' => NodeKindEnum::STRING,
'value' => 'someValue',
],
]);
$this->assertInstanceOf(ObjectFieldNode::class, $node);
}
public function testBuildObjectTypeDefinitionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::OBJECT_TYPE_DEFINITION,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someObject',
],
]);
$this->assertInstanceOf(ObjectTypeDefinitionNode::class, $node);
}
public function testBuildObjectTypeExtensionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::OBJECT_TYPE_EXTENSION,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someOtherObject',
],
]);
$this->assertInstanceOf(ObjectTypeExtensionNode::class, $node);
}
public function testBuildObjectValueNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::OBJECT,
'fields' => [
[
'kind' => NodeKindEnum::FIELD,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someField',
],
'type' => [
'kind' => NodeKindEnum::NAMED_TYPE,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'String',
],
],
],
],
]);
$this->assertInstanceOf(ObjectValueNode::class, $node);
}
public function testBuildOperationDefinitionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::OPERATION_DEFINITION,
'operation' => 'query',
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'Query',
],
]);
$this->assertInstanceOf(OperationDefinitionNode::class, $node);
}
public function testBuildOperationTypeDefinitionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::OPERATION_TYPE_DEFINITION,
'operation' => 'query',
'type' => [
'kind' => NodeKindEnum::NAMED_TYPE,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'String',
],
],
]);
$this->assertInstanceOf(OperationTypeDefinitionNode::class, $node);
}
public function testBuildScalarTypeDefinitionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::SCALAR_TYPE_DEFINITION,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'SomeScalar',
],
]);
$this->assertInstanceOf(ScalarTypeDefinitionNode::class, $node);
}
public function testBuildScalarTypeExtensionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::SCALAR_TYPE_EXTENSION,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'SomeOtherScalar',
],
]);
$this->assertInstanceOf(ScalarTypeExtensionNode::class, $node);
}
public function testBuildSchemaDefinitionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::SCHEMA_DEFINITION,
'operationTypes' => [
[
'kind' => NodeKindEnum::OPERATION_TYPE_DEFINITION,
'operation' => 'query',
'type' => [
'kind' => NodeKindEnum::NAMED_TYPE,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'String',
],
],
],
],
]);
$this->assertInstanceOf(SchemaDefinitionNode::class, $node);
}
public function testBuildSelectionSetNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::SELECTION_SET,
'selections' => [
[
'kind' => NodeKindEnum::FIELD,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someField',
],
'type' => [
'kind' => NodeKindEnum::NAMED_TYPE,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'String',
],
],
],
],
]);
$this->assertInstanceOf(SelectionSetNode::class, $node);
}
public function testBuildStringValueNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::STRING,
'value' => 'someValue',
]);
$this->assertInstanceOf(StringValueNode::class, $node);
}
public function testBuildUnionTypeDefinitionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::UNION_TYPE_DEFINITION,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'SomeUnion',
],
'types' => [
[
'kind' => NodeKindEnum::NAMED_TYPE,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'SomeType',
],
],
[
'kind' => NodeKindEnum::NAMED_TYPE,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'SomeOtherType',
],
],
],
]);
$this->assertInstanceOf(UnionTypeDefinitionNode::class, $node);
}
public function testBuildUnionTypeExtensionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::UNION_TYPE_EXTENSION,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'SomeOtherUnion',
],
'types' => [
[
'kind' => NodeKindEnum::NAMED_TYPE,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'SomeOtherType',
],
],
],
]);
$this->assertInstanceOf(UnionTypeExtensionNode::class, $node);
}
public function testBuildVariableDefinitionNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::VARIABLE_DEFINITION,
'variable' => [
'kind' => NodeKindEnum::VARIABLE,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someVariable',
],
],
'type' => [
'kind' => NodeKindEnum::NAMED_TYPE,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'String',
],
],
]);
$this->assertInstanceOf(VariableDefinitionNode::class, $node);
}
public function testBuildVariableNode()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::VARIABLE,
'name' => [
'kind' => NodeKindEnum::NAME,
'value' => 'someVariable',
],
]);
$this->assertInstanceOf(VariableNode::class, $node);
}
public function testCreateLocation()
{
$node = $this->builder->build([
'kind' => NodeKindEnum::NAME,
'value' => 'SomeType',
'loc' => ['start' => 0, 'end' => 8],
]);
$this->assertInstanceOf(Location::class, $node->getLocation());
}
}
================================================
FILE: tests/Unit/Schema/Resolver/ResolverRegistryTest.php
================================================
[
'human' => function ($_, $args) {
return getHuman($args['id']);
},
'droid' => function ($_, $args) {
return getDroid($args['id']);
},
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertArraySubset([
'name' => 'Luke Skywalker',
], $registry->getFieldResolver('Query', 'human')(null, ['id' => '1000']));
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertArraySubset([
'name' => 'R2-D2',
], $registry->getFieldResolver('Query', 'droid')(null, ['id' => '2001']));
}
public function testTypeResolver()
{
$registry = new ResolverRegistry([
'Query' => new QueryResolver(),
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertArraySubset([
'name' => 'Luke Skywalker',
], $registry->getFieldResolver('Query', 'human')(null, ['id' => '1000']));
}
public function testFieldResolver()
{
$registry = new ResolverRegistry([
'Query' => [
'human' => new HumanResolver(),
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertArraySubset([
'name' => 'Luke Skywalker',
], $registry->getFieldResolver('Query', 'human')(null, ['id' => '1000']));
}
public function testRegisterResolver()
{
$registry = new ResolverRegistry();
$registry->register('Query', new QueryResolver());
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertArraySubset([
'name' => 'Luke Skywalker',
], $registry->getFieldResolver('Query', 'human')(null, ['id' => '1000']));
}
public function testExtendExistingResolver()
{
$registry = new ResolverRegistry([
'Query' => [
'human' => function ($_, $args) {
return getHuman($args['id']);
},
'droid' => function ($_, $args) {
return getDroid($args['id']);
},
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertArraySubset([
'name' => 'Luke Skywalker',
], $registry->getFieldResolver('Query', 'human')(null, ['id' => '1000']));
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertArraySubset([
'name' => 'R2-D2',
], $registry->getFieldResolver('Query', 'droid')(null, ['id' => '2001']));
/** @noinspection PhpUndefinedMethodInspection */
$registry->getResolver('Query')->addResolver('hero', function ($_, $args) {
return getHero($args['episode'] ?? null);
});
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertArraySubset([
'name' => 'Luke Skywalker',
], $registry->getFieldResolver('Query', 'hero')(null, ['episode' => 'EMPIRE']));
}
public function testMiddleware()
{
$messages = [];
$logCallback = function (string $message) use (&$messages) {
$messages[] = $message;
};
$registry = new ResolverRegistry([
'Query' => [
'hello' => new HelloResolver($logCallback),
],
], [
new LogInputMiddleware($logCallback),
new LogResultMiddleware($logCallback)
]);
$result = $registry->getFieldResolver('Query', 'hello')(null, ['name' => 'Bob']);
$this->assertEquals('Hello Bob!', $result);
$this->assertEquals([
'1. logInput {"name":"Bob"}',
'2. logResult',
'3. resolver: hello',
'4. logResult',
'5. logInput',
], $messages);
}
public function testResolverWithSpecificMiddleware()
{
$messages = [];
$logCallback = function (string $message) use (&$messages) {
$messages[] = $message;
};
$registry = new ResolverRegistry([
'Query' => [
'hello' => new HelloResolverWithSpecificMiddleware($logCallback),
],
], [
new LogInputMiddleware($logCallback),
new LogResultMiddleware($logCallback)
]);
$registry->getFieldResolver('Query', 'hello')(null, ['name' => 'Bob']);
$this->assertEquals([
'1. logInput {"name":"Bob"}',
'3. resolver: hello',
'5. logInput',
], $messages);
}
}
abstract class LogMiddleware implements ResolverMiddlewareInterface
{
protected $logCallback;
public function __construct(callable $logCallback)
{
$this->logCallback = $logCallback;
}
}
class LogInputMiddleware extends LogMiddleware
{
/**
* @inheritdoc
*/
public function resolve(
callable $resolveCallback,
$rootValue,
array $arguments,
$context = null,
?ResolveInfo $info = null
) {
\call_user_func($this->logCallback, \sprintf('1. logInput %s', \json_encode($arguments)));
$result = $resolveCallback($rootValue, $arguments, $context, $info);
\call_user_func($this->logCallback, '5. logInput');
return $result;
}
}
class LogResultMiddleware extends LogMiddleware
{
/**
* @inheritdoc
*/
public function resolve(
callable $resolveCallback,
$rootValue,
array $arguments,
$context = null,
?ResolveInfo $info = null
) {
\call_user_func($this->logCallback, '2. logResult');
$result = $resolveCallback($rootValue, $arguments, $context, $info);
\call_user_func($this->logCallback, '4. logResult');
return $result;
}
}
class QueryResolver extends AbstractTypeResolver
{
public function resolveHuman($rootValue, array $arguments, $context = null, ?ResolveInfo $info = null): array
{
return getHuman($arguments['id']);
}
}
class HumanResolver extends AbstractFieldResolver
{
public function resolve($rootValue, array $arguments, $context = null, ?ResolveInfo $info = null): array
{
return getHuman($arguments['id']);
}
}
class HelloResolver extends AbstractFieldResolver
{
protected $logCallback;
public function __construct(callable $logCallback)
{
$this->logCallback = $logCallback;
}
public function resolve($rootValue, array $arguments, $context = null, ?ResolveInfo $info = null): string
{
\call_user_func($this->logCallback, '3. resolver: hello');
return \sprintf('Hello %s!', $arguments['name'] ?? 'world');
}
}
class HelloResolverWithSpecificMiddleware extends HelloResolver
{
public function getMiddleware(): ?array
{
return [
LogInputMiddleware::class,
];
}
}
================================================
FILE: tests/Unit/Type/Coercer/CoercerInterfaceTest.php
================================================
assertInstanceOf(CoercerInterface::class, new IntCoercer());
$this->assertInstanceOf(CoercerInterface::class, new FloatCoercer());
$this->assertInstanceOf(CoercerInterface::class, new StringCoercer());
$this->assertInstanceOf(CoercerInterface::class, new BooleanCoercer());
}
}
================================================
FILE: tests/Unit/Type/Coercer/FloatCoercerTest.php
================================================
coercer = new FloatCoercer();
}
/**
* @throws \Digia\GraphQL\Error\InvalidTypeException
*/
public function testSuccessfulCoercion(): void
{
$this->assertSame(0.0, $this->coercer->coerce(false));
$this->assertSame(1.0, $this->coercer->coerce(true));
$this->assertSame(2.0, $this->coercer->coerce(2));
}
}
================================================
FILE: tests/Unit/Type/Coercer/IntCoercerTest.php
================================================
coercer = new IntCoercer();
}
/**
* @throws \Digia\GraphQL\Error\InvalidTypeException
*/
public function testSuccessfulCoercion(): void
{
$this->assertSame(0, $this->coercer->coerce(false));
$this->assertSame(1, $this->coercer->coerce(true));
$this->assertSame(2, $this->coercer->coerce(2.0));
}
/**
* @param mixed $value
* @dataProvider coerceTooLargeIntegerDataProvider
* @throws InvalidTypeException
*/
public function testCoerceTooLargeInteger($value): void
{
$this->expectException(InvalidTypeException::class);
$this->expectExceptionMessageRegExp('*Int cannot represent non 32-bit signed integer value*');
$this->coercer->coerce($value);
}
/**
* @return array
*/
public function coerceTooLargeIntegerDataProvider(): array
{
return [
['1273898127398213987219837198273232314324324324324324324324324324'],
[1e100],
[-1e100],
];
}
/**
* @throws InvalidTypeException
*/
public function testCoerceFloat(): void
{
$this->expectException(InvalidTypeException::class);
$this->expectExceptionMessageRegExp('*Int cannot represent non-integer value*');
$this->coercer->coerce(4.55);
}
}
================================================
FILE: tests/Unit/Type/Coercer/StringCoercerTest.php
================================================
coercer = new StringCoercer();
}
/**
* @throws \Digia\GraphQL\Error\InvalidTypeException
*/
public function testSuccessfulCoercion(): void
{
$this->assertSame('false', $this->coercer->coerce(false));
$this->assertSame('true', $this->coercer->coerce(true));
$this->assertSame('null', $this->coercer->coerce(null));
$this->assertSame('2', $this->coercer->coerce(2));
$this->assertSame('3.1415926535898', $this->coercer->coerce(pi()));
}
}
================================================
FILE: tests/Unit/Util/AbstractEnumTest.php
================================================
assertSame([
1,
2
], DummyEnum::values());
}
}
/**
* Class DummyEnum
* @package Digia\GraphQL\Test\Unit\Util
*/
class DummyEnum extends AbstractEnum
{
public const VALUE_1 = 1;
public const VALUE_2 = 2;
}
================================================
FILE: tests/Unit/Util/NameHelperTest.php
================================================
assertNull($exception);
}
}
================================================
FILE: tests/Unit/Util/NodeComparatorTest.php
================================================
nodeBuilder = GraphQL::make(NodeBuilderInterface::class);
}
public function testCompareSameNode()
{
$node = $this->nodeBuilder->build([
'kind' => NodeKindEnum::NAME,
'value' => ['kind' => NodeKindEnum::STRING, 'value' => 'Foo'],
]);
$this->assertTrue(NodeComparator::compare($node, $node));
}
public function testCompareDifferentNode()
{
$node = $this->nodeBuilder->build([
'kind' => NodeKindEnum::NAME,
'value' => ['kind' => NodeKindEnum::STRING, 'value' => 'Foo'],
]);
$other = $this->nodeBuilder->build([
'kind' => NodeKindEnum::NAME,
'value' => ['kind' => NodeKindEnum::STRING, 'value' => 'Bar'],
]);
$this->assertFalse(NodeComparator::compare($node, $other));
}
}
================================================
FILE: tests/Unit/Util/ValueASTConverterTest.php
================================================
converter = new ValueASTConverter();
}
public function testConvertNonNullWithStringValue()
{
$node = new StringValueNode('foo', false, null);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertSame('foo', $this->converter->convert($node, newNonNull(stringType())));
}
public function testConvertNonNullWithNullValue()
{
$node = new NullValueNode(null);
$this->expectException(ConversionException::class);
$this->expectExceptionMessage('Cannot convert non-null values from null value node');
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertSame(null, $this->converter->convert($node, newNonNull(stringType())));
}
public function testConvertValidListOfStrings()
{
$node = new ListValueNode(
[
new StringValueNode('A', false, null),
new StringValueNode('B', false, null),
new StringValueNode('C', false, null),
],
null
);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertSame(['A', 'B', 'C'], $this->converter->convert($node, newList(stringType())));
}
public function testConvertListWithMissingVariableValue()
{
$node = new ListValueNode(
[
new VariableNode(new NameNode('$a', null), null),
new VariableNode(new NameNode('$b', null), null),
new VariableNode(new NameNode('$c', null), null),
],
null
);
// Null-able inputs in a variable can be omitted
$variables = ['$a' => 'A', '$c' => 'C'];
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertSame(['A', null, 'C'], $this->converter->convert($node, newList(stringType()), $variables));
}
public function testConvertValidInputObject()
{
$node = new ObjectValueNode(
[new ObjectFieldNode(new NameNode('a', null), new IntValueNode(1, null), null)],
null
);
/** @noinspection PhpUnhandledExceptionInspection */
$type = newInputObjectType([
'name' => 'InputObject',
'fields' => [
'a' => ['type' => intType()],
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertSame(['a' => 1], $this->converter->convert($node, $type));
}
public function testConvertInputObjectWithNodeOfInvalidType()
{
$node = new StringValueNode(null, false, null);
/** @noinspection PhpUnhandledExceptionInspection */
$type = newInputObjectType([
'name' => 'InputObject',
'fields' => [
'a' => ['type' => intType()],
],
]);
$this->expectException(ConversionException::class);
$this->expectExceptionMessage('Input object values can only be converted form object value nodes.');
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertSame(1, $this->converter->convert($node, $type));
}
public function testConvertInputObjectWithMissingNonNullField()
{
$node = new ObjectValueNode(
[new ObjectFieldNode(new NameNode('a', null), new IntValueNode(1, null), null)],
null
);
/** @noinspection PhpUnhandledExceptionInspection */
$type = newInputObjectType([
'name' => 'InputObject',
'fields' => [
'a' => ['type' => intType()],
'b' => ['type' => newNonNull(stringType())],
],
]);
$this->expectException(ConversionException::class);
$this->expectExceptionMessage('Cannot convert input object value for missing non-null field.');
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertSame(['a' => 1], $this->converter->convert($node, $type));
}
public function testConvertEnumWithIntValue()
{
$node = new EnumValueNode('FOO', null);
/** @noinspection PhpUnhandledExceptionInspection */
$type = newEnumType([
'name' => 'EnumType',
'values' => [
'FOO' => ['value' => 1],
],
]);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertSame(1, $this->converter->convert($node, $type));
}
public function testConvertEnumWithNodeOfInvalidType()
{
$node = new StringValueNode(null, false, null);
$type = newEnumType([
'name' => 'EnumType',
]);
$this->expectException(ConversionException::class);
$this->expectExceptionMessage('Enum values can only be converted from enum value nodes.');
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertSame(1, $this->converter->convert($node, $type));
}
public function testConvertEnumWithMissingValue()
{
$node = new EnumValueNode('FOO', null);
/** @noinspection PhpUnhandledExceptionInspection */
$type = newEnumType([
'name' => 'EnumType',
'values' => [
'BAR' => ['value' => 'foo'],
],
]);
$this->expectException(ConversionException::class);
$this->expectExceptionMessage('Cannot convert enum value for missing value "FOO".');
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertSame(1, $this->converter->convert($node, $type));
}
public function testConvertValidScalar()
{
$node = new StringValueNode('foo', false, null);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertSame('foo', $this->converter->convert($node, stringType()));
}
public function testConvertInvalidScalar()
{
$node = new StringValueNode(null, false, null);
$this->expectException(ConversionException::class);
/** @noinspection PhpUnhandledExceptionInspection */
$this->assertSame('foo', $this->converter->convert($node, stringType()));
}
}
================================================
FILE: tests/Unit/Util/ValueConverterTest.php
================================================
valueConverter = new ValueConverter();
}
/**
* @throws \Digia\GraphQL\Error\ConversionException
* @throws \Digia\GraphQL\Error\InvariantException
* @throws \Digia\GraphQL\Error\SyntaxErrorException
*/
public function testIdType(): void
{
// Ensure numerical IDs become Ints
$this->assertInstanceOf(IntValueNode::class, $this->valueConverter->convert('45', idType()));
$this->assertInstanceOf(StringValueNode::class, $this->valueConverter->convert('abc123', idType()));
}
}
================================================
FILE: tests/Unit/Util/ValueHelperTest.php
================================================
valueHelper = new ValueHelper();
}
/**
*
*/
public function testCompareArguments(): void
{
// Test argument count mismatch
$a = [
$this->makeStringArgumentNode('name', 'value'),
$this->makeStringArgumentNode('name', 'value'),
];
$b = [
$this->makeStringArgumentNode('name', 'value'),
];
$this->assertFalse($this->valueHelper->compareArguments($a, $b));
// Test with no matching name value
$a = [
$this->makeStringArgumentNode('name', 'value'),
];
$b = [
$this->makeStringArgumentNode('other name', 'value'),
];
$this->assertFalse($this->valueHelper->compareArguments($a, $b));
// Test with matching name value but mismatching value
$a = [
$this->makeStringArgumentNode('name', 'value'),
];
$b = [
$this->makeStringArgumentNode('name', 'other value'),
];
$this->assertFalse($this->valueHelper->compareArguments($a, $b));
// Test with full match
$a = [
$this->makeStringArgumentNode('name', 'value'),
];
$b = [
$this->makeStringArgumentNode('name', 'value'),
];
$this->assertTrue($this->valueHelper->compareArguments($a, $b));
}
/**
* @param string $name
* @param string $value
* @return ArgumentNode
*/
private function makeStringArgumentNode(string $name, string $value): ArgumentNode
{
return new ArgumentNode(new NameNode($name, null), new StringValueNode($value, false, null), null);
}
}
================================================
FILE: tests/utils.php
================================================