├── .gitignore ├── .scrutinizer.yml ├── .styleci.yml ├── .travis.yml ├── README.md ├── composer.json ├── docs ├── annotations_reference.md ├── authentication_authorization.md ├── custom_output_types.md ├── extend_type.md ├── external_type_declaration.md ├── file_uploads.md ├── getting-started.md ├── inheritance.md ├── input_types.md ├── mutations.md ├── my_first_query.md ├── other_frameworks.md ├── pagination.md ├── symfony-bundle.md ├── troubleshooting.md └── type_mapping.md ├── phpstan.neon ├── phpunit.xml.dist ├── src ├── AggregateControllerQueryProvider.php ├── AggregateQueryProvider.php ├── AnnotationReader.php ├── Annotations │ ├── AbstractRequest.php │ ├── Exceptions │ │ └── ClassNotFoundException.php │ ├── ExtendType.php │ ├── Factory.php │ ├── FailWith.php │ ├── Field.php │ ├── Logged.php │ ├── Mutation.php │ ├── Query.php │ ├── Right.php │ ├── SourceField.php │ ├── SourceFieldInterface.php │ └── Type.php ├── Containers │ ├── BasicAutoWiringContainer.php │ ├── EmptyContainer.php │ └── NotFoundException.php ├── FieldNotFoundException.php ├── FieldsBuilder.php ├── FieldsBuilderFactory.php ├── FromSourceFieldsInterface.php ├── GlobControllerQueryProvider.php ├── GraphQLException.php ├── Hydrators │ ├── CannotHydrateException.php │ ├── FactoryHydrator.php │ └── HydratorInterface.php ├── InputTypeGenerator.php ├── InputTypeUtils.php ├── InvalidDocBlockException.php ├── Mappers │ ├── CannotMapTypeException.php │ ├── CannotMapTypeExceptionInterface.php │ ├── CompositeTypeMapper.php │ ├── DuplicateMappingException.php │ ├── GlobTypeMapper.php │ ├── MappedClass.php │ ├── PorpaginasMissingParameterException.php │ ├── PorpaginasTypeMapper.php │ ├── RecursiveTypeMapper.php │ ├── RecursiveTypeMapperInterface.php │ ├── StaticTypeMapper.php │ └── TypeMapperInterface.php ├── MissingAnnotationException.php ├── MissingTypeHintException.php ├── NamingStrategy.php ├── NamingStrategyInterface.php ├── QueryField.php ├── QueryProviderInterface.php ├── Reflection │ └── CachedDocBlockFactory.php ├── Schema.php ├── SchemaFactory.php ├── Security │ ├── AuthenticationServiceInterface.php │ ├── AuthorizationServiceInterface.php │ ├── FailAuthenticationService.php │ ├── FailAuthorizationService.php │ ├── SecurityNotImplementedException.php │ ├── VoidAuthenticationService.php │ └── VoidAuthorizationService.php ├── TypeGenerator.php ├── TypeMappingException.php ├── TypeRegistry.php └── Types │ ├── CustomTypesRegistry.php │ ├── DateTimeType.php │ ├── ID.php │ ├── InterfaceFromObjectType.php │ ├── InvalidTypesInUnionException.php │ ├── MutableObjectType.php │ ├── ResolvableInputInterface.php │ ├── ResolvableInputObjectType.php │ ├── TypeAnnotatedObjectType.php │ ├── TypeResolver.php │ └── UnionType.php ├── tests ├── AbstractQueryProviderTest.php ├── AggregateControllerQueryProviderTest.php ├── AggregateQueryProviderTest.php ├── AnnotationReaderTest.php ├── Annotations │ ├── ExtendTypeTest.php │ ├── FailWithTest.php │ ├── RightTest.php │ └── TypeTest.php ├── Containers │ ├── BasicAutoWiringContainerTest.php │ └── EmptyContainerTest.php ├── FieldsBuilderTest.php ├── Fixtures │ ├── Annotations │ │ ├── ClassWithInvalidClassAnnotation.php │ │ ├── ClassWithInvalidExtendTypeAnnotation.php │ │ └── ClassWithInvalidTypeAnnotation.php │ ├── BadClassType │ │ └── TestType.php │ ├── DuplicateInputTypes │ │ ├── TestFactory.php │ │ └── TestFactory2.php │ ├── DuplicateTypes │ │ ├── TestType.php │ │ └── TestType2.php │ ├── Integration │ │ ├── Controllers │ │ │ ├── ContactController.php │ │ │ └── ProductController.php │ │ ├── Models │ │ │ ├── Contact.php │ │ │ ├── Product.php │ │ │ └── User.php │ │ └── Types │ │ │ ├── ContactFactory.php │ │ │ ├── ContactType.php │ │ │ └── ExtendedContactType.php │ ├── Interfaces │ │ ├── ClassA.php │ │ ├── ClassB.php │ │ ├── ClassC.php │ │ └── Types │ │ │ ├── ClassAType.php │ │ │ └── ClassBType.php │ ├── TestController.php │ ├── TestControllerNoReturnType.php │ ├── TestControllerWithArrayParam.php │ ├── TestControllerWithArrayReturnType.php │ ├── TestControllerWithFailWith.php │ ├── TestControllerWithInvalidInputType.php │ ├── TestControllerWithInvalidReturnType.php │ ├── TestControllerWithIterableParam.php │ ├── TestControllerWithIterableReturnType.php │ ├── TestObject.php │ ├── TestObject2.php │ ├── TestObjectMissingReturnType.php │ ├── TestType.php │ ├── TestTypeId.php │ ├── TestTypeMissingAnnotation.php │ ├── TestTypeMissingField.php │ ├── TestTypeMissingReturnType.php │ ├── TestTypeWithFailWith.php │ ├── TestTypeWithSourceFieldInterface.php │ ├── TypeFoo.php │ └── Types │ │ ├── AbstractFooType.php │ │ ├── FooExtendType.php │ │ ├── FooType.php │ │ ├── NoClass.php │ │ ├── NoTypeAnnotation.php │ │ └── TestFactory.php ├── GlobControllerQueryProviderTest.php ├── Hydrators │ └── FactoryHydratorTest.php ├── InputTypeUtilsTest.php ├── Integration │ └── EndToEndTest.php ├── Mappers │ ├── CompositeTypeMapperTest.php │ ├── GlobTypeMapperTest.php │ ├── PorpaginasTypeMapperTest.php │ ├── RecursiveTypeMapperTest.php │ └── StaticTypeMapperTest.php ├── NamingStrategyTest.php ├── Reflection │ └── CachedDocBlockFactoryTest.php ├── SchemaFactoryTest.php ├── SchemaTest.php ├── Security │ ├── FailAuthenticationServiceTest.php │ └── FailAuthorizationServiceTest.php ├── TypeGeneratorTest.php ├── TypeRegistryTest.php └── Types │ ├── IDTest.php │ ├── MutableObjectTypeTest.php │ ├── ResolvableInputObjectTypeTest.php │ ├── TypeResolverTest.php │ └── UnionTypeTest.php └── website ├── README.md ├── core └── Footer.js ├── package.json ├── pages └── en │ ├── help.js │ ├── index.js │ └── users.js ├── sidebars.json ├── siteConfig.js └── static ├── css └── custom.css └── img ├── at2.svg ├── graphql-controllers.svg ├── logo.svg ├── oss_logo.png ├── php-fig.jpg ├── query1.png ├── symfony_black_03.svg └── tcm.png /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /mouf/ 3 | /composer.lock 4 | /src/Tests/ 5 | /build/ 6 | /phpunit.xml 7 | 8 | node_modules 9 | 10 | lib/core/metadata.js 11 | lib/core/MetadataBlog.js 12 | 13 | website/translated_docs 14 | website/build/ 15 | website/yarn.lock 16 | website/node_modules 17 | website/i18n/* 18 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | environment: 3 | php: 4 | version: 7.1 5 | nodes: 6 | analysis: 7 | tests: 8 | override: 9 | - php-scrutinizer-run 10 | checks: 11 | php: 12 | code_rating: true 13 | duplication: true 14 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr2 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | cache: 3 | directories: 4 | - $HOME/.composer/cache 5 | matrix: 6 | include: 7 | - php: 7.3 8 | env: PREFER_LOWEST="" 9 | - php: 7.2 10 | env: PREFER_LOWEST="" 11 | - php: 7.1 12 | env: PREFER_LOWEST="" 13 | - php: 7.1 14 | env: PREFER_LOWEST="--prefer-lowest" 15 | 16 | before_script: 17 | - composer update --prefer-dist $PREFER_LOWEST 18 | script: 19 | - "./vendor/bin/phpunit" 20 | - composer phpstan 21 | after_script: 22 | - if [ -z "$PREFER_LOWEST" ]; then ./vendor/bin/coveralls -v; fi 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest Stable Version](https://poser.pugx.org/thecodingmachine/graphql-controllers/v/stable)](https://packagist.org/packages/thecodingmachine/graphql-controllers) 2 | [![Total Downloads](https://poser.pugx.org/thecodingmachine/graphql-controllers/downloads)](https://packagist.org/packages/thecodingmachine/graphql-controllers) 3 | [![Latest Unstable Version](https://poser.pugx.org/thecodingmachine/graphql-controllers/v/unstable)](https://packagist.org/packages/thecodingmachine/graphql-controllers) 4 | [![License](https://poser.pugx.org/thecodingmachine/graphql-controllers/license)](https://packagist.org/packages/thecodingmachine/graphql-controllers) 5 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/thecodingmachine/graphql-controllers/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/thecodingmachine/graphql-controllers/?branch=master) 6 | [![Build Status](https://travis-ci.org/thecodingmachine/graphql-controllers.svg?branch=master)](https://travis-ci.org/thecodingmachine/graphql-controllers) 7 | [![Coverage Status](https://coveralls.io/repos/thecodingmachine/graphql-controllers/badge.svg?branch=master&service=github)](https://coveralls.io/github/thecodingmachine/graphql-controllers?branch=master) 8 | 9 | 10 | # DEPRECATED. 11 | # This library has moved to https://github.com/thecodingmachine/graphqlite 12 | 13 | GraphQL controllers 14 | =================== 15 | 16 | A utility library on top of `webonyx/graphql-php` library. 17 | 18 | Note: v1 and v2 of this library was built on top of `youshido/graphql`. 19 | 20 | This library allows you to write your GraphQL queries in simple-to-write controllers. 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thecodingmachine/graphql-controllers", 3 | "description": "Write your GraphQL queries in simple to write controllers (using webonix/graphql-php).", 4 | "license": "MIT", 5 | "type": "library", 6 | "authors": [ 7 | { 8 | "name": "David Négrier", 9 | "email": "d.negrier@thecodingmachine.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7.1", 14 | "webonyx/graphql-php": "^0.13", 15 | "psr/container": "^1", 16 | "doctrine/annotations": "^1.2", 17 | "doctrine/cache": "^1.8", 18 | "thecodingmachine/class-explorer": "^1.1", 19 | "psr/simple-cache": "^1", 20 | "phpdocumentor/reflection-docblock": "^4.3", 21 | "phpdocumentor/type-resolver": "^0.4", 22 | "psr/http-message": "^1", 23 | "ecodev/graphql-upload": "^4.0" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "^6.1", 27 | "satooshi/php-coveralls": "^1.0", 28 | "symfony/cache": "^4.1.4", 29 | "mouf/picotainer": "^1.1", 30 | "phpstan/phpstan": "^0.11", 31 | "beberlei/porpaginas": "^1.2" 32 | }, 33 | "suggest": { 34 | "beberlei/porpaginas": "If you want automatic pagination in your GraphQL types" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "TheCodingMachine\\GraphQL\\Controllers\\": "src/" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "TheCodingMachine\\GraphQL\\Controllers\\": "tests/" 44 | } 45 | }, 46 | "scripts": { 47 | "phpstan": "phpstan analyse src -c phpstan.neon --level=4 --no-progress -vvv" 48 | }, 49 | "extra": { 50 | "branch-alias": { 51 | "dev-master": "3.0.x-dev" 52 | } 53 | }, 54 | "minimum-stability": "dev", 55 | "prefer-stable": true 56 | } 57 | -------------------------------------------------------------------------------- /docs/custom_output_types.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: custom-output-types 3 | title: Custom output types 4 | sidebar_label: Custom output types 5 | --- 6 | 7 | ## Why do I need this? 8 | 9 | In some special cases, you want to override the GraphQL return type that is attributed by default by GraphQL-Controllers. 10 | 11 | Here is a sample: 12 | 13 | ```php 14 | /** 15 | * @Type(class=Product::class) 16 | */ 17 | class ProductType 18 | { 19 | /** 20 | * @Field(name="id") 21 | */ 22 | public function getId(Product $source): string 23 | { 24 | return $source->getId(); 25 | } 26 | } 27 | ``` 28 | 29 | In the example above, GraphQL-Controllers will generate a GraphQL schema with a field "id" of type "string". 30 | 31 | ```graphql 32 | type Product { 33 | id: String! 34 | } 35 | ``` 36 | 37 | GraphQL comes with an "ID" scalar type. But PHP has no such type. So GraphQL-Controllers does not know when a variable 38 | is an ID or not. 39 | 40 | You can help GraphQL-Controllers by manually specifying the output type to use: 41 | 42 | ```php 43 | /** 44 | * @Field(name="id", outputType="ID") 45 | */ 46 | ``` 47 | 48 | ## Usage 49 | 50 | The **outputType** attribute will map the return value of the method to the output type passed in parameter. 51 | 52 | You can use the **outputType** attribute in the following annotations: 53 | 54 | - `@Query` 55 | - `@Mutation` 56 | - `@Field` 57 | - `@SourceField` 58 | 59 | ## Registering a custom output type (advanced) 60 | 61 | If you have special needs, you can design your own output type. GraphQL-Controllers runs on top of webonyx/graphql. 62 | 63 | In order to create a custom output type, you need to: 64 | 65 | 1. design a class that extends `GraphQL\Type\Definition\ObjectType` 66 | 2. register this class in the GraphQL schema 67 | 68 | In order to [create your custom output type, check out the Webonyx documentation](https://webonyx.github.io/graphql-php/type-system/object-types/). 69 | 70 | In order to find existing types, the schema is using "type mappers" (classes implementing the `TypeMapperInterface` interface). 71 | You need to make sure that one of these type mappers can return an instance of your type. The way you do this will depend on the framework 72 | you use. 73 | 74 | ### Symfony users 75 | 76 | Any class extending `GraphQL\Type\Definition\ObjectType` (and available in the container) will be automatically detected 77 | by Symfony and added to the schema. 78 | 79 | If you want to automatically map the output type to a given PHP class, you will have to explicitly declare the output type 80 | as a service and use the "graphql.output_type" tag: 81 | 82 | ```yaml 83 | # config/services.yaml 84 | services: 85 | App\MyOutputType: 86 | tags: 87 | - { name: 'graphql.output_type', class: 'App\MyPhpClass' } 88 | ``` 89 | 90 | ### Other frameworks 91 | 92 | The easiest way is to use a `StaticTypeMapper`. This class is used to register custom output types. 93 | 94 | ```php 95 | // Sample code: 96 | $staticTypeMapper = new StaticTypeMapper(); 97 | 98 | // Let's register a type that maps by default to the "MyClass" PHP class 99 | $staticTypeMapper->setTypes([ 100 | MyClass::class => new MyCustomOutputType() 101 | ]); 102 | 103 | // If you don't want your output type to map to any PHP class by default, use: 104 | $staticTypeMapper->setNotMappedTypes([ 105 | new MyCustomOutputType() 106 | ]); 107 | 108 | ``` 109 | 110 | **Notice:** The `StaticTypeMapper` instance must be registered in your container and linked to a `CompositeTypeMapper` 111 | that will aggregate all the type mappers of the application. -------------------------------------------------------------------------------- /docs/extend_type.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: extend_type 3 | title: Extending a type 4 | sidebar_label: Extending a type 5 | --- 6 | 7 | Fields exposed in a GraphQL type do not need to be all part of the same class. 8 | 9 | Use the `@ExtendType` annotation to add additional fields to a type that is already declared. 10 | 11 |
Heads up! Extending a type has nothing to do with type inheritance. 12 | If you are looking for a way to expose a class and its children classes, have a look at 13 | the "Inheritance" documentation page 14 |
15 | 16 | ## The @ExtendType annotation 17 | 18 | Let's assume you have a `Product` class. In order to get the name of a product, there is no `getName()` method in 19 | the product because the name needs to be translated in the correct language. You have a `TranslationService` to do that. 20 | 21 | ```php 22 | /** 23 | * @Type() 24 | */ 25 | class Product 26 | { 27 | // ... 28 | 29 | /** 30 | * @Field() 31 | */ 32 | public function getId(): string 33 | { 34 | return $this->id; 35 | } 36 | 37 | /** 38 | * @Field() 39 | */ 40 | public function getPrice(): ?float 41 | { 42 | return $this->price; 43 | } 44 | } 45 | ``` 46 | 47 | ```php 48 | // You need to use a service to get the name of the product in the correct language. 49 | $name = $translationService->getProductName($productId, $language); 50 | ``` 51 | 52 | Using `@ExtendType`, you can add an additional `name` field to your product: 53 | 54 | ```php 55 | use TheCodingMachine\GraphQL\Controllers\Annotations\ExtendType; 56 | use TheCodingMachine\GraphQL\Controllers\Annotations\Field; 57 | use App\Entities\Product; 58 | 59 | /** 60 | * @ExtendType(class=Product::class) 61 | */ 62 | class ProductType 63 | { 64 | private $translationService; 65 | 66 | public function __construct(TranslationServiceInterface $translationService) 67 | { 68 | $this->translationService = $translationService; 69 | } 70 | 71 | /** 72 | * @Field() 73 | */ 74 | public function getName(Product $product, string $language): string 75 | { 76 | return $this->translationService->getProductName($product->getId(), $language); 77 | } 78 | } 79 | ``` 80 | 81 | Let's break this sample: 82 | 83 | ```php 84 | /** 85 | * @ExtendType(class=Product::class) 86 | */ 87 | ``` 88 | 89 | With the `@ExtendType` annotation, we tell GraphQL-Controllers that we want to add fields in the GraphQL type mapped to 90 | the `Product` PHP class. 91 | 92 | ```php 93 | class ProductType 94 | { 95 | private $translationService; 96 | 97 | public function __construct(TranslationServiceInterface $translationService) 98 | { 99 | $this->translationService = $translationService; 100 | } 101 | 102 | // ... 103 | } 104 | ``` 105 | 106 | 107 | - The `ProductType` class must be in the types namespace. You configured this namespace when you installed GraphQL-Controllers. 108 | - The `ProductType` class is actually a **service**. You can therefore inject dependencies in it (like the `$translationService` in this example) 109 | 110 |
Heads up! The ProductType class must exist in the container of your 111 | application and the container identifier MUST be the fully qualified class name.

112 | If you are using the Symfony bundle (or a framework with autowiring like Laravel), this 113 | is usually not an issue as the container will automatically create the controller entry if you do not explicitly 114 | declare it.
115 | 116 | ```php 117 | /** 118 | * @Field() 119 | */ 120 | public function getName(Product $product, string $language): string 121 | { 122 | return $this->translationService->getProductName($product->getId(), $language); 123 | } 124 | ``` 125 | 126 | The `@Field` annotation is used to add the "name" field to the `Product` type. 127 | 128 | Take a close look at the signature. The first parameter is the "resolved object" we are working on. 129 | Any additional parameters are used as arguments. 130 | 131 | Using the "[Type language](https://graphql.org/learn/schema/#type-language)" notation, we defined a type extension for 132 | the GraphQL "Product" type: 133 | 134 | ```graphql 135 | Extend type Product { 136 | name(language: !String): String! 137 | } 138 | ``` 139 | 140 |
Type extension is a very powerful tool. Use it to add fields that needs to be 141 | computed from services not available in the entity. 142 |
143 | -------------------------------------------------------------------------------- /docs/file_uploads.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: file-uploads 3 | title: File uploads 4 | sidebar_label: File uploads 5 | --- 6 | 7 | GraphQL does not support natively the notion of file uploads, but an extension to the GraphQL protocol was proposed 8 | to add support for [multipart requests](https://github.com/jaydenseric/graphql-multipart-request-spec). 9 | 10 | GraphQL-Controllers supports this extension through the use of the [Ecodev/graphql-upload third party library](https://github.com/Ecodev/graphql-upload). 11 | 12 | ## Installation 13 | 14 | The file upload feature relies on a third party middleware (ecodev/graphql-upload). You need additional steps 15 | to install it. 16 | 17 | ### If you are using the Symfony bundle 18 | 19 | If you are using our Symfony bundle, the file upload middleware is managed by the bundle. You have nothing to do 20 | and can start using it right away. 21 | 22 | ### If you are using a PSR-15 compatible framework 23 | 24 | In order to use this, you must first be sure that the ecodev/graphql-upload PSR-15 middleware is part of your middleware pipe. 25 | 26 | Simply add `GraphQL\Upload\UploadMiddleware` to your middleware pipe. 27 | 28 | ### If you are using another framework not compatible with PSR-15 29 | 30 | Please check the [Ecodev/graphql-upload third party library documentation](https://github.com/Ecodev/graphql-upload) 31 | for more information on how to integrate it in your library. 32 | 33 | ## Usage 34 | 35 | To handle an uploaded file, you type-hint against the PSR-7 `UploadedFileInterface`: 36 | 37 | 38 | ```php 39 | class MyController 40 | { 41 | /** 42 | * @Mutation 43 | */ 44 | public function saveDocument(string $name, UploadedFileInterface $file): Document 45 | { 46 | // Some code that saves the document. 47 | $file->moveTo($someDir); 48 | } 49 | } 50 | ``` 51 | 52 | Of course, you need to use a GraphQL client that is compatible with multipart requests. 53 | 54 | The [jaydenseric/graphql-multipart-request-spec project contains a list of compatible clients](https://github.com/jaydenseric/graphql-multipart-request-spec#client) 55 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: getting-started 3 | title: Getting started 4 | sidebar_label: Getting Started 5 | --- 6 | 7 | GraphQL-Controllers is a framework agnostic library. You can use it in any PHP projects as long a you know how to 8 | inject services in your favorite framework's container. 9 | 10 | Currently, we provide an additional bundle to help you get started with Symfony. More frameworks will be supported soon. 11 | 12 | - [Get started with Symfony](symfony-bundle.md) 13 | - [Get started with another framework (or no framework)](other_frameworks.md) 14 | -------------------------------------------------------------------------------- /docs/inheritance.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: inheritance 3 | title: Inheritance and interfaces 4 | sidebar_label: Inheritance and interfaces 5 | --- 6 | 7 | Some of your entities may extend other entities. GraphQL-controllers will do its best to represent this hierarchy of objects in GraphQL using interfaces. 8 | 9 | Let's say you have 2 classes: `Contact` and `User` (which extends `Contact`) 10 | 11 | ```php 12 | /** 13 | * @Type 14 | */ 15 | class Contact 16 | { 17 | // ... 18 | } 19 | 20 | /** 21 | * @Type 22 | */ 23 | class User extends Contact 24 | { 25 | // ... 26 | } 27 | ``` 28 | 29 | Both classes are also declared as GraphQL types (using the `@Type` annotation). 30 | 31 | Now, let's assume you have a query that returns a contact: 32 | 33 | ``` 34 | class ContactController 35 | { 36 | /** 37 | * @Query() 38 | */ 39 | public function getContact(): Contact 40 | { 41 | // ... 42 | } 43 | } 44 | ``` 45 | 46 | When writing a GraphQL query, you can query using fragments: 47 | 48 | ```graphql 49 | contact { 50 | name 51 | ... User { 52 | email 53 | } 54 | } 55 | ``` 56 | 57 | Behind the scene, GraphQL-controllers will detect that the `Contact` class is extended by the `User` class. Because the 58 | class is extended, a GraphQL `ContactInterface` interface is created dynamically. You don't have to do anything. 59 | The GraphQL `User` type will automatically implement this `ContactInterface`. The interface contains all the fields 60 | available in the `Contact` type. 61 | 62 | Written in "[GraphQL type language](https://graphql.org/learn/schema/#type-language)", the representation of types 63 | would look like this: 64 | 65 | ```graphql 66 | interface ContactInterface { 67 | // List of fields declared in Contact class 68 | } 69 | 70 | type Contact implements ContactInterface { 71 | // List of fields declared in Contact class 72 | } 73 | 74 | type User implements ContactInterface { 75 | // List of fields declared in Contact and User classes 76 | } 77 | ``` 78 | 79 |
Right now, there is no way to explicitly declare a GraphQL interface using GraphQL-Controllers. 80 | GraphQL-Controllers automatically declares interfaces when it sees an inheritance relationship between to classes that 81 | are GraphQL types. 82 |
83 | -------------------------------------------------------------------------------- /docs/input_types.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: input-types 3 | title: Input types 4 | sidebar_label: Input types 5 | --- 6 | 7 | Let's admit you are developing an API that returns a list of cities around a location. 8 | 9 | Your GraphQL query might look like this: 10 | 11 | ```php 12 | class MyController 13 | { 14 | /** 15 | * @Query 16 | * @return City[] 17 | */ 18 | public function getCities(Location $location, float $radius): array 19 | { 20 | // Some code that returns an array of cities. 21 | } 22 | } 23 | 24 | // Class Location is a simple value-object. 25 | class Location 26 | { 27 | private $latitude; 28 | private $longitude; 29 | 30 | public function __construct(float $latitude, float $longitude) 31 | { 32 | $this->latitude = $latitude; 33 | $this->longitude = $longitude; 34 | } 35 | 36 | public function getLatitude(): float 37 | { 38 | return $this->latitude; 39 | } 40 | 41 | public function getLongitude(): float 42 | { 43 | return $this->longitude; 44 | } 45 | } 46 | ``` 47 | 48 | If you try to run this code, you will get the following error: 49 | 50 | ``` 51 | CannotMapTypeException: cannot map class "Location" to a known GraphQL input type. Check your TypeMapper configuration. 52 | ``` 53 | 54 | You are running into this error because GraphQL-Controllers does not know how to handle the `Location` object. 55 | 56 | In GraphQL, an object passed in parameter of a query or mutation (or any field) is called an **Input Type**. 57 | 58 | In order to declare that type, in GraphQL-Controllers, we will declare a **Factory**. 59 | 60 | A **Factory** is a method that takes in parameter all the fields of the input type and return an object. 61 | 62 | Here is an example of factory: 63 | 64 | ``` 65 | class MyFactory 66 | { 67 | /** 68 | * The Factory annotation will create automatically a LocationInput input type in GraphQL. 69 | * 70 | * @Factory() 71 | */ 72 | public function createLocation(float $latitude, float $longitude): Location 73 | { 74 | return new Location($latitude, $longitude); 75 | } 76 | } 77 | ``` 78 | 79 | and now, you can run query like this: 80 | 81 | ``` 82 | mutation { 83 | getCities(location: { 84 | latitude: 45.0, 85 | longitude: 0.0, 86 | }, 87 | radius: 42) 88 | { 89 | id, 90 | name 91 | } 92 | } 93 | ``` 94 | 95 | - Factories must be declared with the **@Factory** annotation. 96 | - The parameters of the factories are the field of the GraphQL input type 97 | 98 | A few important things to notice: 99 | 100 | - The container MUST contain the factory class. The identifier of the factory MUST be the fully qualified class name of the class that contains the factory. 101 | This is usually already the case if you are using a container with auto-wiring capabilities 102 | - We recommend that you put the factories in the same directories as the types. 103 | 104 | ### Specifying the input type name 105 | 106 | The GraphQL input type name is derived from the return type of the factory. 107 | 108 | Given the factory below, the return type is "Location", therefore, the GraphQL input type will be named "LocationInput". 109 | 110 | ``` 111 | /** 112 | * @Factory() 113 | */ 114 | public function createLocation(float $latitude, float $longitude): Location 115 | { 116 | return new Location($latitude, $longitude); 117 | } 118 | ``` 119 | 120 | In case you want to override the input type name, you can use the "name" attribute of the @Factory annotation: 121 | 122 | ``` 123 | /** 124 | * @Factory(name="MyNewInputName") 125 | */ 126 | ``` 127 | 128 | Most of the time, the input type name will be completely transparent to you, so there is no real reason 129 | to want to customize it. 130 | -------------------------------------------------------------------------------- /docs/mutations.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: mutations 3 | title: Writing mutations 4 | sidebar_label: Mutations 5 | --- 6 | 7 | In GraphQL-Controllers, mutations are created [just like queries](my_first_query.md). 8 | 9 | To create a mutation, you annotate a method in a controller with the `@Mutation` annotation. 10 | 11 | Here is a sample of a "saveProduct" query: 12 | 13 | ```php 14 | namespace App\Controllers; 15 | 16 | use TheCodingMachine\GraphQL\Controllers\Annotations\Mutation; 17 | 18 | class ProductController 19 | { 20 | /** 21 | * @Mutation 22 | */ 23 | public function saveProduct(int $id, string $name, ?float $price = null): Product 24 | { 25 | // Some code that saves a product. 26 | } 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/pagination.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: pagination 3 | title: Paginating large result sets 4 | sidebar_label: Pagination 5 | --- 6 | 7 | It is quite common to have to paginate over large result sets. 8 | 9 | GraphQL-Controllers offers a simple way to do that using [Porpaginas](https://github.com/beberlei/porpaginas). 10 | 11 | Porpaginas is a set of PHP interfaces that can be implemented by result iterators. It comes with a native support for 12 | PHP arrays, Doctrine and [TDBM](https://thecodingmachine.github.io/tdbm/doc/limit_offset_resultset.html). 13 | 14 | ## Usage 15 | 16 | In your query, simply return a class that implements `Porpaginas\Result`: 17 | 18 | ```php 19 | class MyController 20 | { 21 | /** 22 | * @Query 23 | * @return Product[] 24 | */ 25 | public function products(): Porpaginas\Result 26 | { 27 | // Some code that returns a list of products 28 | 29 | // If you are using Doctrine, something like: 30 | return new Porpaginas\ORMQueryResult($doctrineQuery); 31 | } 32 | } 33 | ``` 34 | 35 | Notice that: 36 | 37 | - the method return type MUST BE `Porpaginas\Result` or a class implementing `Porpaginas\Result` 38 | - you MUST add a `@return` statement to help GraphQL-Controllers find the type of the list 39 | 40 | Once this is done, you can paginate directly from your GraphQL query: 41 | 42 | ``` 43 | products { 44 | items(limit: 10, offset: 20) { 45 | id 46 | name 47 | } 48 | count 49 | } 50 | ``` 51 | 52 | Results are wrapped into an item field. You can use the "limit" and "offset" parameters to apply pagination automatically. 53 | 54 | The "count" field returns the **total count** of items. 55 | -------------------------------------------------------------------------------- /docs/symfony-bundle.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: symfony-bundle 3 | title: Getting started with Symfony 4 | sidebar_label: Symfony bundle 5 | --- 6 | 7 | The GraphQL-Controllers bundle is compatible with Symfony 4+. 8 | From your Symfony 4+ project, run the following command: 9 | 10 | Applications that use Symfony Flex 11 | ---------------------------------- 12 | 13 | Open a command console, enter your project directory and execute: 14 | 15 | ```console 16 | $ composer require thecodingmachine/graphql-controllers-bundle 17 | ``` 18 | 19 | Applications that don't use Symfony Flex 20 | ---------------------------------------- 21 | 22 | ### Step 1: Download the Bundle 23 | 24 | Open a command console, enter your project directory and execute the 25 | following command to download the latest stable version of this bundle: 26 | 27 | ```console 28 | $ composer require thecodingmachine/graphql-controllers-bundle 29 | ``` 30 | 31 | This command requires you to have Composer installed globally, as explained 32 | in the [installation chapter](https://getcomposer.org/doc/00-intro.md) 33 | of the Composer documentation. 34 | 35 | ### Step 2: Enable the Bundle 36 | 37 | Then, enable the bundle by adding it to the list of registered bundles 38 | in the `app/AppKernel.php` file of your project: 39 | 40 | ```php 41 | DateTime PHP class is not supported. 112 | Only the DateTimeImmutable PHP class is mapped. 113 | 114 |
This is ok:
115 | 116 | ```php 117 | /** 118 | * @Query 119 | * @return Product[] 120 | */ 121 | public function getProducts(\DateTimeImmutable $fromDate): array 122 | { 123 | 124 | } 125 | ``` 126 | 127 |
But DateTime input type is not supported:
128 | 129 | ```php 130 | /** 131 | * @Query 132 | * @return Product[] 133 | */ 134 | public function getProducts(\DateTime $fromDate): array // BAD 135 | { 136 | 137 | } 138 | ``` 139 | 140 | ## Union types 141 | -------------- 142 | 143 | You can create a GraphQL union type "on the fly", using the pipe (|) operator in the PHPDoc: 144 | 145 | ```php 146 | /** 147 | * @Query 148 | * @return Company|Contact <== can return a company OR a contact 149 | */ 150 | public function companyOrContact(int $id) 151 | { 152 | // Some code that returns a company or a contact. 153 | } 154 | ``` 155 | 156 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - "#PHPDoc tag \\@throws with type Psr\\\\Container\\\\ContainerExceptionInterface is not subtype of Throwable#" 4 | - "#Property TheCodingMachine\\\\GraphQL\\\\Controllers\\\\Types\\\\ResolvableInputObjectType::$resolve \\(array&callable\\) does not accept array#" 5 | - "#Variable \\$failWith might not be defined#" 6 | #includes: 7 | # - vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | ./tests/ 17 | 18 | 19 | 20 | 21 | 22 | src/ 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/AggregateControllerQueryProvider.php: -------------------------------------------------------------------------------- 1 | controllers = $controllers; 39 | $this->queryProviderFactory = $queryProviderFactory; 40 | $this->controllersContainer = $controllersContainer; 41 | $this->recursiveTypeMapper = $recursiveTypeMapper; 42 | } 43 | 44 | /** 45 | * @return QueryField[] 46 | */ 47 | public function getQueries(): array 48 | { 49 | $queryList = []; 50 | 51 | foreach ($this->controllers as $controllerName) { 52 | $controller = $this->controllersContainer->get($controllerName); 53 | $queryProvider = $this->queryProviderFactory->buildFieldsBuilder($this->recursiveTypeMapper); 54 | $queryList = array_merge($queryList, $queryProvider->getQueries($controller)); 55 | } 56 | 57 | return $queryList; 58 | } 59 | 60 | /** 61 | * @return QueryField[] 62 | */ 63 | public function getMutations(): array 64 | { 65 | $mutationList = []; 66 | 67 | foreach ($this->controllers as $controllerName) { 68 | $controller = $this->controllersContainer->get($controllerName); 69 | $queryProvider = $this->queryProviderFactory->buildFieldsBuilder($this->recursiveTypeMapper); 70 | $mutationList = array_merge($mutationList, $queryProvider->getMutations($controller)); 71 | } 72 | 73 | return $mutationList; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/AggregateQueryProvider.php: -------------------------------------------------------------------------------- 1 | queryProviders = is_array($queryProviders) ? $queryProviders : iterator_to_array($queryProviders); 27 | } 28 | 29 | /** 30 | * @return QueryField[] 31 | */ 32 | public function getQueries(): array 33 | { 34 | $queriesArray = array_map(function(QueryProviderInterface $queryProvider) { return $queryProvider->getQueries(); }, $this->queryProviders); 35 | if ($queriesArray === []) { 36 | return []; 37 | } 38 | return array_merge(...$queriesArray); 39 | } 40 | 41 | /** 42 | * @return QueryField[] 43 | */ 44 | public function getMutations(): array 45 | { 46 | $mutationsArray = array_map(function(QueryProviderInterface $queryProvider) { return $queryProvider->getMutations(); }, $this->queryProviders); 47 | if ($mutationsArray === []) { 48 | return []; 49 | } 50 | return array_merge(...$mutationsArray); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Annotations/AbstractRequest.php: -------------------------------------------------------------------------------- 1 | outputType = $attributes['outputType'] ?? null; 25 | $this->name = $attributes['name'] ?? null; 26 | } 27 | 28 | /** 29 | * Returns the GraphQL return type of the request (as a string). 30 | * The string can represent the FQCN of the type or an entry in the container resolving to the GraphQL type. 31 | * 32 | * @return string|null 33 | */ 34 | public function getOutputType(): ?string 35 | { 36 | return $this->outputType; 37 | } 38 | 39 | /** 40 | * Returns the name of the GraphQL query/mutation/field. 41 | * If not specified, the name of the method should be used instead. 42 | * 43 | * @return null|string 44 | */ 45 | public function getName(): ?string 46 | { 47 | return $this->name; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Annotations/Exceptions/ClassNotFoundException.php: -------------------------------------------------------------------------------- 1 | getMessage()." defined in @Type annotation of class '$className'"); 19 | } 20 | 21 | public static function wrapExceptionForExtendTag(self $e, string $className): self 22 | { 23 | return new self($e->getMessage()." defined in @ExtendType annotation of class '$className'"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Annotations/ExtendType.php: -------------------------------------------------------------------------------- 1 | class = $attributes['class']; 36 | if (!class_exists($this->class)) { 37 | throw ClassNotFoundException::couldNotFindClass($this->class); 38 | } 39 | } 40 | 41 | /** 42 | * Returns the name of the GraphQL query/mutation/field. 43 | * If not specified, the name of the method should be used instead. 44 | * 45 | * @return string 46 | */ 47 | public function getClass(): string 48 | { 49 | return ltrim($this->class, '\\'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Annotations/Factory.php: -------------------------------------------------------------------------------- 1 | name = $attributes['name'] ?? null; 28 | } 29 | 30 | /** 31 | * Returns the name of the GraphQL input type. 32 | * If not specified, the name of the method should be used instead. 33 | * 34 | * @return null|string 35 | */ 36 | public function getName(): ?string 37 | { 38 | return $this->name; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Annotations/FailWith.php: -------------------------------------------------------------------------------- 1 | $values 23 | * 24 | * @throws \BadMethodCallException 25 | */ 26 | public function __construct(array $values) 27 | { 28 | if (!array_key_exists('value', $values)) { 29 | throw new \BadMethodCallException('The @FailWith annotation must be passed a defaultValue. For instance: "@FailWith(null)"'); 30 | } 31 | $this->value = $values['value']; 32 | } 33 | 34 | /** 35 | * Returns the default value to use if the right is not enforced. 36 | * 37 | * @return mixed 38 | */ 39 | public function getValue() 40 | { 41 | return $this->value; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Annotations/Field.php: -------------------------------------------------------------------------------- 1 | $values 22 | * 23 | * @throws \BadMethodCallException 24 | */ 25 | public function __construct(array $values) 26 | { 27 | if (!isset($values['value']) && !isset($values['name'])) { 28 | throw new \BadMethodCallException('The @Right annotation must be passed a right name. For instance: "@Right(\'my_right\')"'); 29 | } 30 | $this->name = $values['value'] ?? $values['name']; 31 | } 32 | 33 | /** 34 | * @return string 35 | */ 36 | public function getName(): string 37 | { 38 | return $this->name; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Annotations/SourceField.php: -------------------------------------------------------------------------------- 1 | name = $attributes['name'] ?? null; 67 | $this->logged = $attributes['logged'] ?? false; 68 | $this->right = $attributes['right'] ?? null; 69 | $this->outputType = $attributes['outputType'] ?? null; 70 | $this->id = $attributes['isId'] ?? false; 71 | if (array_key_exists('failWith', $attributes)) { 72 | $this->failWith = $attributes['failWith']; 73 | $this->hasFailWith = true; 74 | } 75 | } 76 | 77 | /** 78 | * Returns the GraphQL right to be applied to this source field. 79 | * 80 | * @return Right|null 81 | */ 82 | public function getRight(): ?Right 83 | { 84 | return $this->right; 85 | } 86 | 87 | /** 88 | * Returns the name of the GraphQL query/mutation/field. 89 | * If not specified, the name of the method should be used instead. 90 | * 91 | * @return null|string 92 | */ 93 | public function getName(): ?string 94 | { 95 | return $this->name; 96 | } 97 | 98 | /** 99 | * @return bool 100 | */ 101 | public function isLogged(): bool 102 | { 103 | return $this->logged; 104 | } 105 | 106 | /** 107 | * Returns the GraphQL return type of the request (as a string). 108 | * The string is the GraphQL output type name. 109 | * 110 | * @return string|null 111 | */ 112 | public function getOutputType(): ?string 113 | { 114 | return $this->outputType; 115 | } 116 | 117 | /** 118 | * If the GraphQL type is "ID", isID will return true. 119 | * 120 | * @return bool 121 | */ 122 | public function isId(): bool 123 | { 124 | return $this->id; 125 | } 126 | 127 | /** 128 | * Returns the default value to use if the right is not enforced. 129 | * 130 | * @return mixed 131 | */ 132 | public function getFailWith() 133 | { 134 | return $this->failWith; 135 | } 136 | 137 | /** 138 | * True if a default value is available if a right is not enforced. 139 | * 140 | * @return bool 141 | */ 142 | public function canFailWith(): bool 143 | { 144 | return $this->hasFailWith; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Annotations/SourceFieldInterface.php: -------------------------------------------------------------------------------- 1 | setClass($attributes['class']); 42 | if (!class_exists($this->class)) { 43 | throw ClassNotFoundException::couldNotFindClass($this->class); 44 | } 45 | } else { 46 | $this->selfType = true; 47 | } 48 | } 49 | 50 | /** 51 | * Returns the fully qualified class name of the targeted class. 52 | * 53 | * @return string 54 | */ 55 | public function getClass(): string 56 | { 57 | if ($this->class === null) { 58 | throw new \RuntimeException('Empty class for @Type annotation. You MUST create the Type annotation object using the GraphQL-Controllers AnnotationReader'); 59 | } 60 | return $this->class; 61 | } 62 | 63 | public function setClass(string $class): void 64 | { 65 | $this->class = ltrim($class, '\\'); 66 | } 67 | 68 | public function isSelfType(): bool 69 | { 70 | return $this->selfType; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Containers/BasicAutoWiringContainer.php: -------------------------------------------------------------------------------- 1 | container = $container; 33 | } 34 | 35 | /** 36 | * Finds an entry of the container by its identifier and returns it. 37 | * 38 | * @param string $id Identifier of the entry to look for. 39 | * 40 | * @throws NotFoundExceptionInterface No entry was found for **this** identifier. 41 | * @throws ContainerExceptionInterface Error while retrieving the entry. 42 | * 43 | * @return mixed Entry. 44 | */ 45 | public function get($id) 46 | { 47 | if (isset($this->values[$id])) { 48 | return $this->values[$id]; 49 | } 50 | if ($this->container->has($id)) { 51 | return $this->container->get($id); 52 | } 53 | 54 | // The container will try to instantiate the type if the class exists and has an annotation. 55 | if (class_exists($id)) { 56 | $refTypeClass = new \ReflectionClass($id); 57 | if ($refTypeClass->hasMethod('__construct') && $refTypeClass->getMethod('__construct')->getNumberOfRequiredParameters() > 0) { 58 | throw NotFoundException::notFoundInContainer($id); 59 | } 60 | $this->values[$id] = new $id(); 61 | return $this->values[$id]; 62 | } 63 | 64 | throw NotFoundException::notFound($id); 65 | } 66 | 67 | /** 68 | * Returns true if the container can return an entry for the given identifier. 69 | * Returns false otherwise. 70 | * 71 | * `has($id)` returning true does not mean that `get($id)` will not throw an exception. 72 | * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. 73 | * 74 | * @param string $id Identifier of the entry to look for. 75 | * 76 | * @return bool 77 | */ 78 | public function has($id) 79 | { 80 | if (isset($this->values[$id])) { 81 | return true; 82 | } 83 | if ($this->container->has($id)) { 84 | return true; 85 | } 86 | 87 | if (class_exists($id)) { 88 | $refTypeClass = new \ReflectionClass($id); 89 | return !($refTypeClass->hasMethod('__construct') && $refTypeClass->getMethod('__construct')->getNumberOfRequiredParameters() > 0); 90 | } 91 | return false; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Containers/EmptyContainer.php: -------------------------------------------------------------------------------- 1 | getMessage()), 0, $e); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/FieldsBuilderFactory.php: -------------------------------------------------------------------------------- 1 | annotationReader = $annotationReader; 51 | $this->hydrator = $hydrator; 52 | $this->authenticationService = $authenticationService; 53 | $this->authorizationService = $authorizationService; 54 | $this->typeResolver = $typeResolver; 55 | $this->cachedDocBlockFactory = $cachedDocBlockFactory; 56 | $this->namingStrategy = $namingStrategy; 57 | } 58 | 59 | /** 60 | * @param RecursiveTypeMapperInterface $typeMapper 61 | * @return FieldsBuilder 62 | */ 63 | public function buildFieldsBuilder(RecursiveTypeMapperInterface $typeMapper): FieldsBuilder 64 | { 65 | return new FieldsBuilder( 66 | $this->annotationReader, 67 | $typeMapper, 68 | $this->hydrator, 69 | $this->authenticationService, 70 | $this->authorizationService, 71 | $this->typeResolver, 72 | $this->cachedDocBlockFactory, 73 | $this->namingStrategy 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/FromSourceFieldsInterface.php: -------------------------------------------------------------------------------- 1 | resolve($data); 29 | } 30 | throw CannotHydrateException::createForInputType($type->name); 31 | } 32 | 33 | /** 34 | * Whether this hydrate can hydrate the passed data. 35 | * 36 | * @param mixed[] $data 37 | * @param InputObjectType $type 38 | * @return bool 39 | */ 40 | public function canHydrate(array $data, InputObjectType $type): bool 41 | { 42 | return $type instanceof ResolvableInputObjectType; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Hydrators/HydratorInterface.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | private $cache = []; 32 | /** 33 | * @var HydratorInterface 34 | */ 35 | private $hydrator; 36 | /** 37 | * @var InputTypeUtils 38 | */ 39 | private $inputTypeUtils; 40 | 41 | public function __construct(InputTypeUtils $inputTypeUtils, 42 | FieldsBuilderFactory $fieldsBuilderFactory, 43 | HydratorInterface $hydrator) 44 | { 45 | $this->inputTypeUtils = $inputTypeUtils; 46 | $this->fieldsBuilderFactory = $fieldsBuilderFactory; 47 | $this->hydrator = $hydrator; 48 | } 49 | 50 | /** 51 | * @param object $factory 52 | * @param string $methodName 53 | * @param RecursiveTypeMapperInterface $recursiveTypeMapper 54 | * @return InputObjectType 55 | */ 56 | public function mapFactoryMethod($factory, string $methodName, RecursiveTypeMapperInterface $recursiveTypeMapper): InputObjectType 57 | { 58 | $method = new ReflectionMethod($factory, $methodName); 59 | 60 | [$inputName, $className] = $this->inputTypeUtils->getInputTypeNameAndClassName($method); 61 | 62 | if (!isset($this->cache[$inputName])) { 63 | // TODO: add comment argument. 64 | $this->cache[$inputName] = new ResolvableInputObjectType($inputName, $this->fieldsBuilderFactory, $recursiveTypeMapper, $factory, $methodName, $this->hydrator, null); 65 | } 66 | 67 | return $this->cache[$inputName]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/InputTypeUtils.php: -------------------------------------------------------------------------------- 1 | annotationReader = $annotationReader; 25 | $this->namingStrategy = $namingStrategy; 26 | } 27 | 28 | /** 29 | * Returns an array with 2 elements: [ $inputName, $className ] 30 | * 31 | * @param ReflectionMethod $method 32 | * @return string[] 33 | */ 34 | public function getInputTypeNameAndClassName(ReflectionMethod $method): array 35 | { 36 | $fqsen = ltrim((string) $this->validateReturnType($method), '\\'); 37 | $factory = $this->annotationReader->getFactoryAnnotation($method); 38 | if ($factory === null) { 39 | throw new \RuntimeException($method->getDeclaringClass()->getName().'::'.$method->getName().' has no @Factory annotation.'); 40 | } 41 | return [$this->namingStrategy->getInputTypeName($fqsen, $factory), $fqsen]; 42 | } 43 | 44 | private function validateReturnType(ReflectionMethod $refMethod): Fqsen 45 | { 46 | $returnType = $refMethod->getReturnType(); 47 | if ($returnType === null) { 48 | throw MissingTypeHintException::missingReturnType($refMethod); 49 | } 50 | 51 | if ($returnType->allowsNull()) { 52 | throw MissingTypeHintException::nullableReturnType($refMethod); 53 | } 54 | 55 | $type = (string) $returnType; 56 | 57 | $typeResolver = new \phpDocumentor\Reflection\TypeResolver(); 58 | 59 | $phpdocType = $typeResolver->resolve($type); 60 | if (!$phpdocType instanceof Object_) { 61 | throw MissingTypeHintException::invalidReturnType($refMethod); 62 | } 63 | 64 | return $phpdocType->getFqsen(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/InvalidDocBlockException.php: -------------------------------------------------------------------------------- 1 | getDeclaringClass()->getName().'::'.$refMethod->getName().' has several @return annotations.'); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Mappers/CannotMapTypeException.php: -------------------------------------------------------------------------------- 1 | getName(), 31 | $parameter->getDeclaringClass()->getName(), 32 | $parameter->getDeclaringFunction()->getName(), 33 | $previous->getMessage()); 34 | 35 | return new self($message, 0, $previous); 36 | } 37 | 38 | public static function wrapWithReturnInfo(CannotMapTypeExceptionInterface $previous, ReflectionMethod $method): self 39 | { 40 | $message = sprintf('For return type of %s::%s, %s', 41 | $method->getDeclaringClass()->getName(), 42 | $method->getName(), 43 | $previous->getMessage()); 44 | 45 | return new self($message, 0, $previous); 46 | } 47 | 48 | public static function mustBeOutputType($subTypeName): self 49 | { 50 | return new self('type "'.$subTypeName.'" must be an output type.'); 51 | } 52 | 53 | public static function createForExtendType(string $className, ObjectType $type): self 54 | { 55 | return new self('cannot extend GraphQL type "'.$type->name.'" mapped by class "'.$className.'". Check your TypeMapper configuration.'); 56 | } 57 | 58 | public static function createForExtendName(string $name, ObjectType $type): self 59 | { 60 | return new self('cannot extend GraphQL type "'.$type->name.'" with type "'.$name.'". Check your TypeMapper configuration.'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Mappers/CannotMapTypeExceptionInterface.php: -------------------------------------------------------------------------------- 1 | className = $className; 27 | }*/ 28 | 29 | /** 30 | * @return string 31 | */ 32 | /*public function getClassName(): string 33 | { 34 | return $this->className; 35 | }*/ 36 | 37 | /** 38 | * @return MappedClass|null 39 | */ 40 | /*public function getParent(): ?MappedClass 41 | { 42 | return $this->parent; 43 | }*/ 44 | 45 | /** 46 | * @param MappedClass|null $parent 47 | */ 48 | /*public function setParent(?MappedClass $parent): void 49 | { 50 | $this->parent = $parent; 51 | }*/ 52 | 53 | /** 54 | * @return MappedClass[] 55 | */ 56 | public function getChildren(): array 57 | { 58 | return $this->children; 59 | } 60 | 61 | /** 62 | * @param MappedClass $child 63 | */ 64 | public function addChild(MappedClass $child): void 65 | { 66 | $this->children[] = $child; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Mappers/PorpaginasMissingParameterException.php: -------------------------------------------------------------------------------- 1 | 79 | */ 80 | public function getOutputTypes(): array; 81 | 82 | /** 83 | * Returns true if this type mapper can map the $typeName GraphQL name to a GraphQL type. 84 | * 85 | * @param string $typeName The name of the GraphQL type 86 | * @return bool 87 | */ 88 | public function canMapNameToType(string $typeName): bool; 89 | 90 | /** 91 | * Returns a GraphQL type by name (can be either an input or output type) 92 | * 93 | * @param string $typeName The name of the GraphQL type 94 | * @return Type&(InputType|OutputType) 95 | */ 96 | public function mapNameToType(string $typeName): Type; 97 | } 98 | -------------------------------------------------------------------------------- /src/MissingAnnotationException.php: -------------------------------------------------------------------------------- 1 | getName(), $parameter->getDeclaringClass()->getName(), $parameter->getDeclaringFunction()->getName())); 12 | } 13 | 14 | public static function missingReturnType(ReflectionMethod $method): self 15 | { 16 | return new self(sprintf('Factory "%s::%s" must have a return type.', $method->getDeclaringClass()->getName(), $method->getName())); 17 | } 18 | 19 | public static function invalidReturnType(ReflectionMethod $method): self 20 | { 21 | return new self(sprintf('The return type of factory "%s::%s" must be an object, "%s" passed instead.', $method->getDeclaringClass()->getName(), $method->getName(), $method->getReturnType())); 22 | } 23 | 24 | public static function nullableReturnType(ReflectionMethod $method): self 25 | { 26 | return new self(sprintf('Factory "%s::%s" must have a non nullable return type.', $method->getDeclaringClass()->getName(), $method->getName())); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/NamingStrategy.php: -------------------------------------------------------------------------------- 1 | getClass(); 39 | if ($prevPos = strrpos($typeClassName, '\\')) { 40 | $typeClassName = substr($typeClassName, $prevPos + 1); 41 | } 42 | return $typeClassName; 43 | } 44 | 45 | public function getInputTypeName(string $className, Factory $factory): string 46 | { 47 | $inputTypeName = $factory->getName(); 48 | if ($inputTypeName !== null) { 49 | return $inputTypeName; 50 | } 51 | if ($prevPos = strrpos($className, '\\')) { 52 | $className = substr($className, $prevPos + 1); 53 | } 54 | return $className.'Input'; 55 | } 56 | 57 | /** 58 | * Returns the name of a GraphQL field from the name of the annotated method. 59 | */ 60 | public function getFieldNameFromMethodName(string $methodName): string 61 | { 62 | // Let's remove any "get" or "is". 63 | if (strpos($methodName, 'get') === 0 && strlen($methodName) > 3) { 64 | return lcfirst(substr($methodName, 3)); 65 | } 66 | if (strpos($methodName, 'is') === 0 && strlen($methodName) > 2) { 67 | return lcfirst(substr($methodName, 2)); 68 | } 69 | return $methodName; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/NamingStrategyInterface.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | private $docBlockArrayCache = []; 31 | /** 32 | * @var array 33 | */ 34 | private $contextArrayCache = []; 35 | /** 36 | * @var ContextFactory 37 | */ 38 | private $contextFactory; 39 | 40 | /** 41 | * @param CacheInterface $cache The cache we fetch data from. Note this is a SAFE cache. It does not need to be purged. 42 | * @param DocBlockFactory|null $docBlockFactory 43 | */ 44 | public function __construct(CacheInterface $cache, ?DocBlockFactory $docBlockFactory = null) 45 | { 46 | $this->cache = $cache; 47 | $this->docBlockFactory = $docBlockFactory ?: DocBlockFactory::createInstance(); 48 | $this->contextFactory = new ContextFactory(); 49 | } 50 | 51 | /** 52 | * Fetches a DocBlock object from a ReflectionMethod 53 | * 54 | * @param ReflectionMethod $refMethod 55 | * @return DocBlock 56 | */ 57 | public function getDocBlock(ReflectionMethod $refMethod): DocBlock 58 | { 59 | $key = 'docblock_'.md5($refMethod->getDeclaringClass()->getName().'::'.$refMethod->getName()); 60 | if (isset($this->docBlockArrayCache[$key])) { 61 | return $this->docBlockArrayCache[$key]; 62 | } 63 | 64 | if ($cacheItem = $this->cache->get($key)) { 65 | [ 66 | 'time' => $time, 67 | 'docblock' => $docBlock 68 | ] = $cacheItem; 69 | 70 | if (filemtime($refMethod->getFileName()) === $time) { 71 | $this->docBlockArrayCache[$key] = $docBlock; 72 | return $docBlock; 73 | } 74 | } 75 | 76 | $docBlock = $this->doGetDocBlock($refMethod); 77 | 78 | $this->cache->set($key, [ 79 | 'time' => filemtime($refMethod->getFileName()), 80 | 'docblock' => $docBlock 81 | ]); 82 | $this->docBlockArrayCache[$key] = $docBlock; 83 | 84 | return $docBlock; 85 | } 86 | 87 | private function doGetDocBlock(ReflectionMethod $refMethod): DocBlock 88 | { 89 | $docComment = $refMethod->getDocComment() ?: '/** */'; 90 | 91 | $refClass = $refMethod->getDeclaringClass(); 92 | $refClassName = $refClass->getName(); 93 | 94 | if (!isset($this->contextArrayCache[$refClassName])) { 95 | $this->contextArrayCache[$refClassName] = $this->contextFactory->createFromReflector($refMethod); 96 | } 97 | 98 | return $this->docBlockFactory->create($docComment, $this->contextArrayCache[$refClassName]); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Schema.php: -------------------------------------------------------------------------------- 1 | 'Query', 29 | 'fields' => function() use ($queryProvider) { 30 | $queries = $queryProvider->getQueries(); 31 | if (empty($queries)) { 32 | return [ 33 | 'dummyQuery' => [ 34 | 'type' => Type::string(), 35 | 'description' => 'A placeholder query used by thecodingmachine/graphql-controllers when there are no declared queries.', 36 | 'resolve' => function () { 37 | return 'This is a placeholder query. Please create a query using the @Query annotation.'; 38 | } 39 | ] 40 | ]; 41 | } 42 | return $queries; 43 | } 44 | ]); 45 | $mutation = new ObjectType([ 46 | 'name' => 'Mutation', 47 | 'fields' => function() use ($queryProvider) { 48 | $mutations = $queryProvider->getMutations(); 49 | if (empty($mutations)) { 50 | return [ 51 | 'dummyMutation' => [ 52 | 'type' => Type::string(), 53 | 'description' => 'A placeholder query used by thecodingmachine/graphql-controllers when there are no declared mutations.', 54 | 'resolve' => function () { 55 | return 'This is a placeholder mutation. Please create a mutation using the @Mutation annotation.'; 56 | } 57 | ] 58 | ]; 59 | } 60 | return $mutations; 61 | } 62 | ]); 63 | 64 | $config->setQuery($query); 65 | $config->setMutation($mutation); 66 | 67 | $config->setTypes(function() use ($recursiveTypeMapper) { 68 | return $recursiveTypeMapper->getOutputTypes(); 69 | }); 70 | 71 | $config->setTypeLoader(function(string $name) use ($recursiveTypeMapper, $query, $mutation) { 72 | // We need to find a type FROM a GraphQL type name 73 | if ($name === 'Query') { 74 | return $query; 75 | } 76 | if ($name === 'Mutation') { 77 | return $mutation; 78 | } 79 | 80 | $type = CustomTypesRegistry::mapNameToType($name); 81 | if ($type !== null) { 82 | return $type; 83 | } 84 | 85 | return $recursiveTypeMapper->mapNameToType($name); 86 | }); 87 | 88 | $typeResolver->registerSchema($this); 89 | 90 | parent::__construct($config); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Security/AuthenticationServiceInterface.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | private $outputTypes = []; 22 | 23 | /** 24 | * Registers a type. 25 | * IMPORTANT: the type MUST be fully computed (so ExtendType annotations must have ALREADY been applied to the tag) 26 | * ONLY THE RecursiveTypeMapper IS ALLOWED TO CALL THIS METHOD. 27 | * 28 | * @param NamedType&Type&(ObjectType|InterfaceType) $type 29 | */ 30 | public function registerType(NamedType $type): void 31 | { 32 | if (isset($this->outputTypes[$type->name])) { 33 | throw new GraphQLException('Type "'.$type->name.'" is already registered'); 34 | } 35 | $this->outputTypes[$type->name] = $type; 36 | } 37 | 38 | public function hasType(string $typeName): bool 39 | { 40 | return isset($this->outputTypes[$typeName]); 41 | } 42 | 43 | /** 44 | * @param string $typeName 45 | * @return NamedType&Type&(ObjectType|InterfaceType) 46 | */ 47 | public function getType(string $typeName): NamedType 48 | { 49 | if (!isset($this->outputTypes[$typeName])) { 50 | throw new GraphQLException('Could not find type "'.$typeName.'" in registry'); 51 | } 52 | return $this->outputTypes[$typeName]; 53 | } 54 | 55 | public function getMutableObjectType(string $typeName): MutableObjectType 56 | { 57 | $type = $this->getType($typeName); 58 | if (!$type instanceof MutableObjectType) { 59 | throw new GraphQLException('Expected GraphQL type "'.$typeName.'" to be an MutableObjectType. Got a '.get_class($type)); 60 | } 61 | return $type; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Types/CustomTypesRegistry.php: -------------------------------------------------------------------------------- 1 | format(DateTime::ATOM); 49 | } 50 | 51 | /** 52 | * @param mixed $value 53 | */ 54 | public function parseValue($value): ?DateTimeImmutable 55 | { 56 | return DateTimeImmutable::createFromFormat(DateTime::ATOM, $value) ?: null; 57 | } 58 | 59 | /** 60 | * Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input 61 | * 62 | * In the case of an invalid node or value this method must throw an Exception 63 | * 64 | * @param Node $valueNode 65 | * @param array|null $variables 66 | * @return mixed 67 | * @throws \Exception 68 | */ 69 | public function parseLiteral($valueNode, array $variables = null) 70 | { 71 | if ($valueNode instanceof StringValueNode) { 72 | return $valueNode->value; 73 | } 74 | 75 | return null; 76 | } 77 | } -------------------------------------------------------------------------------- /src/Types/ID.php: -------------------------------------------------------------------------------- 1 | value = $value; 21 | } 22 | 23 | /** 24 | * @return bool|float|int|string 25 | */ 26 | public function val() 27 | { 28 | return $this->value; 29 | } 30 | 31 | public function __toString() 32 | { 33 | return (string) $this->value; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Types/InterfaceFromObjectType.php: -------------------------------------------------------------------------------- 1 | $name, 22 | 'fields' => function() use ($type) { 23 | return $type->getFields(); 24 | }, 25 | 'description' => $type->description, 26 | 'resolveType' => function($value) use ($typeMapper, $subType) { 27 | if (!is_object($value)) { 28 | throw new \InvalidArgumentException('Expected object for resolveType. Got: "'.gettype($value).'"'); 29 | } 30 | 31 | $className = get_class($value); 32 | 33 | return $typeMapper->mapClassToType($className, $subType); 34 | } 35 | ]); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Types/InvalidTypesInUnionException.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | private $fieldsCallables = []; 31 | 32 | /** 33 | * @var FieldDefinition[]|null 34 | */ 35 | private $finalFields; 36 | 37 | public function __construct(array $config) 38 | { 39 | $this->status = self::STATUS_PENDING; 40 | 41 | parent::__construct($config); 42 | } 43 | 44 | public function freeze(): void 45 | { 46 | $this->status = self::STATUS_FROZEN; 47 | } 48 | 49 | public function getStatus(): string 50 | { 51 | return $this->status; 52 | } 53 | 54 | public function addFields(callable $fields): void 55 | { 56 | if ($this->status !== self::STATUS_PENDING) { 57 | throw new \RuntimeException('Tried to add fields to a frozen MutableObjectType.'); 58 | } 59 | $this->fieldsCallables[] = $fields; 60 | } 61 | 62 | /** 63 | * @param string $name 64 | * 65 | * @return FieldDefinition 66 | * 67 | * @throws Exception 68 | */ 69 | public function getField($name): FieldDefinition 70 | { 71 | if ($this->status === self::STATUS_PENDING) { 72 | throw new \RuntimeException('You must freeze() a MutableObjectType before fetching its fields.'); 73 | } 74 | return parent::getField($name); 75 | } 76 | 77 | /** 78 | * @param string $name 79 | * 80 | * @return bool 81 | */ 82 | public function hasField($name): bool 83 | { 84 | if ($this->status === self::STATUS_PENDING) { 85 | throw new \RuntimeException('You must freeze() a MutableObjectType before fetching its fields.'); 86 | } 87 | return parent::hasField($name); 88 | } 89 | 90 | /** 91 | * @return FieldDefinition[] 92 | * 93 | * @throws InvariantViolation 94 | */ 95 | public function getFields(): array 96 | { 97 | if ($this->finalFields === null) { 98 | if ($this->status === self::STATUS_PENDING) { 99 | throw new \RuntimeException('You must freeze() a MutableObjectType before fetching its fields.'); 100 | } 101 | 102 | $this->finalFields = parent::getFields(); 103 | foreach ($this->fieldsCallables as $fieldsCallable) { 104 | $this->finalFields = FieldDefinition::defineFieldMap($this, $fieldsCallable()) + $this->finalFields; 105 | } 106 | } 107 | 108 | return $this->finalFields; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Types/ResolvableInputInterface.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | private $resolve; 34 | 35 | /** 36 | * QueryField constructor. 37 | * @param string $name 38 | * @param FieldsBuilderFactory $controllerQueryProviderFactory 39 | * @param RecursiveTypeMapperInterface $recursiveTypeMapper 40 | * @param object $factory 41 | * @param string $methodName 42 | * @param HydratorInterface $hydrator 43 | * @param null|string $comment 44 | * @param array $additionalConfig 45 | */ 46 | public function __construct(string $name, FieldsBuilderFactory $controllerQueryProviderFactory, RecursiveTypeMapperInterface $recursiveTypeMapper, $factory, string $methodName, HydratorInterface $hydrator, ?string $comment, array $additionalConfig = []) 47 | { 48 | $this->hydrator = $hydrator; 49 | $this->resolve = [ $factory, $methodName ]; 50 | 51 | $fields = function() use ($controllerQueryProviderFactory, $factory, $methodName, $recursiveTypeMapper) { 52 | $method = new ReflectionMethod($factory, $methodName); 53 | $fieldProvider = $controllerQueryProviderFactory->buildFieldsBuilder($recursiveTypeMapper); 54 | return $fieldProvider->getInputFields($method); 55 | }; 56 | 57 | $config = [ 58 | 'name' => $name, 59 | 'fields' => $fields, 60 | ]; 61 | if ($comment) { 62 | $config['description'] = $comment; 63 | } 64 | 65 | $config += $additionalConfig; 66 | parent::__construct($config); 67 | } 68 | 69 | /** 70 | * @param array $args 71 | * @return object 72 | */ 73 | public function resolve(array $args) 74 | { 75 | $toPassArgs = []; 76 | foreach ($this->getFields() as $name => $field) { 77 | $type = $field->getType(); 78 | if (isset($args[$name])) { 79 | $val = $args[$name]; 80 | 81 | $type = $this->stripNonNullType($type); 82 | if ($type instanceof ListOfType) { 83 | $subtype = $this->stripNonNullType($type->getWrappedType()); 84 | $val = array_map(function ($item) use ($subtype) { 85 | if ($subtype instanceof DateTimeType) { 86 | return new \DateTimeImmutable($item); 87 | } elseif ($subtype instanceof InputObjectType) { 88 | return $this->hydrator->hydrate($item, $subtype); 89 | } 90 | return $item; 91 | }, $val); 92 | } elseif ($type instanceof DateTimeType) { 93 | $val = new \DateTimeImmutable($val); 94 | } elseif ($type instanceof InputObjectType) { 95 | $val = $this->hydrator->hydrate($val, $type); 96 | } 97 | } elseif ($field->defaultValueExists()) { 98 | $val = $field->defaultValue; 99 | } else { 100 | throw new GraphQLException("Expected argument '$name' was not provided in GraphQL input type '".$this->name."' used in factory '".get_class($this->resolve[0]).'::'.$this->resolve[1]."()'"); 101 | } 102 | 103 | $toPassArgs[] = $val; 104 | } 105 | 106 | $resolve = $this->resolve; 107 | 108 | return $resolve(...$toPassArgs); 109 | } 110 | 111 | private function stripNonNullType(Type $type): Type 112 | { 113 | if ($type instanceof NonNull) { 114 | return $this->stripNonNullType($type->getWrappedType()); 115 | } 116 | return $type; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Types/TypeAnnotatedObjectType.php: -------------------------------------------------------------------------------- 1 | className = $className; 22 | 23 | parent::__construct($config); 24 | } 25 | 26 | public static function createFromAnnotatedClass(string $typeName, string $className, $annotatedObject, FieldsBuilderFactory $fieldsBuilderFactory, RecursiveTypeMapperInterface $recursiveTypeMapper): self 27 | { 28 | return new self($className, [ 29 | 'name' => $typeName, 30 | 'fields' => function() use ($annotatedObject, $recursiveTypeMapper, $className, $fieldsBuilderFactory) { 31 | $parentClass = get_parent_class($className); 32 | $parentType = null; 33 | if ($parentClass !== false) { 34 | if ($recursiveTypeMapper->canMapClassToType($parentClass)) { 35 | $parentType = $recursiveTypeMapper->mapClassToType($parentClass, null); 36 | } 37 | } 38 | 39 | $fieldProvider = $fieldsBuilderFactory->buildFieldsBuilder($recursiveTypeMapper); 40 | if ($annotatedObject !== null) { 41 | $fields = $fieldProvider->getFields($annotatedObject); 42 | } else { 43 | $fields = $fieldProvider->getSelfFields($className); 44 | } 45 | if ($parentType !== null) { 46 | $fields = $parentType->getFields() + $fields; 47 | } 48 | return $fields; 49 | }, 50 | 'interfaces' => function() use ($className, $recursiveTypeMapper) { 51 | return $recursiveTypeMapper->findInterfaces($className); 52 | } 53 | ]); 54 | } 55 | 56 | public function getMappedClassName(): string 57 | { 58 | return $this->className; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Types/TypeResolver.php: -------------------------------------------------------------------------------- 1 | schema = $schema; 27 | } 28 | 29 | /** 30 | * @param string $typeName 31 | * @return Type 32 | * @throws CannotMapTypeExceptionInterface 33 | */ 34 | public function mapNameToType(string $typeName): Type 35 | { 36 | if ($this->schema === null) { 37 | throw new RuntimeException('You must register a schema first before resolving types.'); 38 | } 39 | 40 | $type = $this->schema->getType($typeName); 41 | if ($type === null) { 42 | throw CannotMapTypeException::createForName($typeName); 43 | } 44 | 45 | return $type; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Types/UnionType.php: -------------------------------------------------------------------------------- 1 | name; 21 | if (!$type instanceof ObjectType) { 22 | throw InvalidTypesInUnionException::notObjectType(); 23 | } 24 | } 25 | parent::__construct([ 26 | 'name' => $name, 27 | 'types' => $types, 28 | 'resolveType' => function($value) use ($typeMapper) { 29 | if (!is_object($value)) { 30 | throw new \InvalidArgumentException('Expected object for resolveType. Got: "'.gettype($value).'"'); 31 | } 32 | 33 | $className = get_class($value); 34 | return $typeMapper->mapClassToInterfaceOrType($className, null); 35 | } 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/AggregateControllerQueryProviderTest.php: -------------------------------------------------------------------------------- 1 | $controller ]) implements ContainerInterface { 21 | 22 | /** 23 | * @var array 24 | */ 25 | private $controllers; 26 | 27 | public function __construct(array $controllers) 28 | { 29 | $this->controllers = $controllers; 30 | } 31 | 32 | public function get($id) 33 | { 34 | return $this->controllers[$id]; 35 | } 36 | 37 | public function has($id) 38 | { 39 | return isset($this->controllers[$id]); 40 | } 41 | }; 42 | 43 | $aggregateQueryProvider = new AggregateControllerQueryProvider([ 'controller' ], $this->getControllerQueryProviderFactory(), $this->getTypeMapper(), $container); 44 | 45 | $queries = $aggregateQueryProvider->getQueries(); 46 | $this->assertCount(6, $queries); 47 | 48 | $mutations = $aggregateQueryProvider->getMutations(); 49 | $this->assertCount(1, $mutations); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/AggregateQueryProviderTest.php: -------------------------------------------------------------------------------- 1 | newInstanceWithoutConstructor() ]; 17 | } 18 | 19 | public function getMutations(): array 20 | { 21 | $queryFieldRef = new ReflectionClass(QueryField::class); 22 | return [ $queryFieldRef->newInstanceWithoutConstructor() ]; 23 | } 24 | }; 25 | } 26 | 27 | public function testGetMutations() 28 | { 29 | $aggregateQueryProvider = new AggregateQueryProvider([$this->getMockQueryProvider(), $this->getMockQueryProvider()]); 30 | $this->assertCount(2, $aggregateQueryProvider->getMutations()); 31 | 32 | $aggregateQueryProvider = new AggregateQueryProvider([]); 33 | $this->assertCount(0, $aggregateQueryProvider->getMutations()); 34 | } 35 | 36 | public function testGetQueries() 37 | { 38 | $aggregateQueryProvider = new AggregateQueryProvider([$this->getMockQueryProvider(), $this->getMockQueryProvider()]); 39 | $this->assertCount(2, $aggregateQueryProvider->getQueries()); 40 | 41 | $aggregateQueryProvider = new AggregateQueryProvider([]); 42 | $this->assertCount(0, $aggregateQueryProvider->getQueries()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Annotations/ExtendTypeTest.php: -------------------------------------------------------------------------------- 1 | expectException(BadMethodCallException::class); 14 | $this->expectExceptionMessage('In annotation @ExtendType, missing compulsory parameter "class".'); 15 | new ExtendType([]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Annotations/FailWithTest.php: -------------------------------------------------------------------------------- 1 | expectException(BadMethodCallException::class); 14 | $this->expectExceptionMessage('The @FailWith annotation must be passed a defaultValue. For instance: "@FailWith(null)"'); 15 | new FailWith([]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Annotations/RightTest.php: -------------------------------------------------------------------------------- 1 | expectException(BadMethodCallException::class); 14 | $this->expectExceptionMessage('The @Right annotation must be passed a right name. For instance: "@Right(\'my_right\')"'); 15 | new Right([]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Annotations/TypeTest.php: -------------------------------------------------------------------------------- 1 | expectException(RuntimeException::class); 14 | $this->expectExceptionMessage('Empty class for @Type annotation. You MUST create the Type annotation object using the GraphQL-Controllers AnnotationReader'); 15 | $type->getClass(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Containers/BasicAutoWiringContainerTest.php: -------------------------------------------------------------------------------- 1 | buildAutoWiringContainer($this->getContainer()); 32 | 33 | $this->assertTrue($container->has('foo')); 34 | $this->assertFalse($container->has('bar')); 35 | 36 | $this->assertSame('foo', $container->get('foo')); 37 | } 38 | 39 | public function testInstantiate() 40 | { 41 | $container = $this->buildAutoWiringContainer($this->getContainer()); 42 | 43 | $this->assertTrue($container->has(TestType::class)); 44 | $type = $container->get(TestType::class); 45 | $this->assertInstanceOf(TestType::class, $type); 46 | $this->assertSame($type, $container->get(TestType::class)); 47 | $this->assertTrue($container->has(TestType::class)); 48 | } 49 | 50 | public function testNotFound() 51 | { 52 | $container = $this->buildAutoWiringContainer($this->getContainer()); 53 | $this->expectException(NotFoundException::class); 54 | $container->get('notfound'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Containers/EmptyContainerTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($container->has('foo')); 14 | $this->expectException(NotFoundExceptionInterface::class); 15 | $container->get('foo'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Fixtures/Annotations/ClassWithInvalidClassAnnotation.php: -------------------------------------------------------------------------------- 1 | name = $name; 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function getName(): string 42 | { 43 | return $this->name; 44 | } 45 | 46 | public function getManager(): ?Contact 47 | { 48 | return $this->manager; 49 | } 50 | 51 | /** 52 | * @param Contact|null $manager 53 | */ 54 | public function setManager(?Contact $manager): void 55 | { 56 | $this->manager = $manager; 57 | } 58 | 59 | /** 60 | * @return Contact[] 61 | */ 62 | public function getRelations(): array 63 | { 64 | return $this->relations; 65 | } 66 | 67 | /** 68 | * @param Contact[] $relations 69 | */ 70 | public function setRelations(array $relations): void 71 | { 72 | $this->relations = $relations; 73 | } 74 | 75 | /** 76 | * @return UploadedFileInterface 77 | */ 78 | public function getPhoto(): UploadedFileInterface 79 | { 80 | return $this->photo; 81 | } 82 | 83 | /** 84 | * @param UploadedFileInterface $photo 85 | */ 86 | public function setPhoto(UploadedFileInterface $photo): void 87 | { 88 | $this->photo = $photo; 89 | } 90 | 91 | /** 92 | * @return DateTimeInterface 93 | */ 94 | public function getBirthDate(): DateTimeInterface 95 | { 96 | return $this->birthDate; 97 | } 98 | 99 | /** 100 | * @param DateTimeInterface $birthDate 101 | */ 102 | public function setBirthDate(DateTimeInterface $birthDate): void 103 | { 104 | $this->birthDate = $birthDate; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/Fixtures/Integration/Models/Product.php: -------------------------------------------------------------------------------- 1 | name = $name; 34 | $this->price = $price; 35 | } 36 | 37 | /** 38 | * @Field(name="name") 39 | * @return string 40 | */ 41 | public function getName(): string 42 | { 43 | return $this->name; 44 | } 45 | 46 | /** 47 | * @Field() 48 | * @return float 49 | */ 50 | public function getPrice(): float 51 | { 52 | return $this->price; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Fixtures/Integration/Models/User.php: -------------------------------------------------------------------------------- 1 | email = $email; 23 | } 24 | 25 | /** 26 | * @Field(name="email") 27 | * @return string 28 | */ 29 | public function getEmail(): string 30 | { 31 | return $this->email; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Fixtures/Integration/Types/ContactFactory.php: -------------------------------------------------------------------------------- 1 | setPhoto($photo); 26 | } 27 | $contact->setBirthDate($birthDate); 28 | $contact->setManager($manager); 29 | $contact->setRelations($relations); 30 | return $contact; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Fixtures/Integration/Types/ContactType.php: -------------------------------------------------------------------------------- 1 | getName()); 27 | } 28 | } -------------------------------------------------------------------------------- /tests/Fixtures/Integration/Types/ExtendedContactType.php: -------------------------------------------------------------------------------- 1 | getName()); 24 | } 25 | } -------------------------------------------------------------------------------- /tests/Fixtures/Interfaces/ClassA.php: -------------------------------------------------------------------------------- 1 | foo = $foo; 16 | } 17 | 18 | public function getFoo(): string 19 | { 20 | return $this->foo; 21 | } 22 | } -------------------------------------------------------------------------------- /tests/Fixtures/Interfaces/ClassB.php: -------------------------------------------------------------------------------- 1 | bar = $bar; 18 | } 19 | 20 | public function getBar(): string 21 | { 22 | return $this->bar; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Fixtures/Interfaces/ClassC.php: -------------------------------------------------------------------------------- 1 | getTest(); 36 | } 37 | return new TestObject($string.$int.$str.($boolean?'true':'false').$float.$dateTimeImmutable->format('YmdHis').$dateTime->format('YmdHis').$withDefault.($id !== null ? $id->val() : '')); 38 | } 39 | 40 | /** 41 | * @Mutation 42 | * @param TestObject $testObject 43 | * @return TestObject 44 | */ 45 | public function mutation(TestObject $testObject): TestObject 46 | { 47 | return $testObject; 48 | } 49 | 50 | /** 51 | * @Query 52 | * @Logged 53 | */ 54 | public function testLogged(): TestObject 55 | { 56 | return new TestObject('foo'); 57 | } 58 | 59 | /** 60 | * @Query 61 | * @Right(name="CAN_FOO") 62 | */ 63 | public function testRight(): TestObject 64 | { 65 | return new TestObject('foo'); 66 | } 67 | 68 | /** 69 | * @Query(outputType="ID") 70 | */ 71 | public function testFixReturnType(): TestObject 72 | { 73 | return new TestObject('foo'); 74 | } 75 | 76 | /** 77 | * @Query(name="nameFromAnnotation") 78 | */ 79 | public function testNameFromAnnotation(): TestObject 80 | { 81 | return new TestObject('foo'); 82 | } 83 | 84 | /** 85 | * @Query(name="arrayObject") 86 | * @return ArrayObject|TestObject[] 87 | */ 88 | public function testArrayObject(): ArrayObject 89 | { 90 | return new ArrayObject([]); 91 | } 92 | 93 | /** 94 | * @Query(name="arrayObject") 95 | * @return iterable|TestObject[] 96 | */ 97 | public function testIterable(): iterable 98 | { 99 | return array(); 100 | } 101 | 102 | /** 103 | * @Query(name="union") 104 | * @return TestObject|TestObject2 105 | */ 106 | public function testUnion() 107 | { 108 | return new TestObject2('foo'); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/Fixtures/TestControllerNoReturnType.php: -------------------------------------------------------------------------------- 1 | test = $test; 20 | $this->testBool = $testBool; 21 | } 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function getTest(): string 27 | { 28 | return $this->test; 29 | } 30 | 31 | /** 32 | * @return bool 33 | */ 34 | public function isTestBool(): bool 35 | { 36 | return $this->testBool; 37 | } 38 | 39 | /** 40 | * @return ?string 41 | */ 42 | public function testRight() 43 | { 44 | return "foo"; 45 | } 46 | 47 | public function getSibling(self $foo): self 48 | { 49 | return new self('foo'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Fixtures/TestObject2.php: -------------------------------------------------------------------------------- 1 | test2 = $test2; 16 | } 17 | 18 | /** 19 | * @return string 20 | */ 21 | public function getTest2(): string 22 | { 23 | return $this->test2; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Fixtures/TestObjectMissingReturnType.php: -------------------------------------------------------------------------------- 1 | getTest().$param; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Fixtures/TestTypeId.php: -------------------------------------------------------------------------------- 1 | 'test']), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Fixtures/TypeFoo.php: -------------------------------------------------------------------------------- 1 | getTest().$param; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Fixtures/Types/FooExtendType.php: -------------------------------------------------------------------------------- 1 | getTest()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Fixtures/Types/FooType.php: -------------------------------------------------------------------------------- 1 | format('Y-m-d').'-'.implode('-', $stringList).'-'.count($dateList)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/GlobControllerQueryProviderTest.php: -------------------------------------------------------------------------------- 1 | $controller ]) implements ContainerInterface { 16 | /** 17 | * @var array 18 | */ 19 | private $controllers; 20 | 21 | public function __construct(array $controllers) 22 | { 23 | $this->controllers = $controllers; 24 | } 25 | 26 | public function get($id) 27 | { 28 | return $this->controllers[$id]; 29 | } 30 | 31 | public function has($id) 32 | { 33 | return isset($this->controllers[$id]); 34 | } 35 | }; 36 | 37 | $globControllerQueryProvider = new GlobControllerQueryProvider('TheCodingMachine\\GraphQL\\Controllers', $this->getControllerQueryProviderFactory(), $this->getTypeMapper(), $container, new NullCache()); 38 | 39 | $queries = $globControllerQueryProvider->getQueries(); 40 | $this->assertCount(6, $queries); 41 | 42 | $mutations = $globControllerQueryProvider->getMutations(); 43 | $this->assertCount(1, $mutations); 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Hydrators/FactoryHydratorTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder(ResolvableInputObjectType::class) 15 | ->disableOriginalConstructor() 16 | ->setMethods(['resolve']) 17 | ->getMock(); 18 | $resolvableInputObjectType->method('resolve')->willReturn(new stdClass()); 19 | 20 | $badObjectType = new InputObjectType([ 21 | 'name' => 'foo' 22 | ]); 23 | 24 | $factoryHydrator = new FactoryHydrator(); 25 | 26 | $this->assertTrue($factoryHydrator->canHydrate([], $resolvableInputObjectType)); 27 | $this->assertFalse($factoryHydrator->canHydrate([], $badObjectType)); 28 | 29 | $this->assertEquals(new stdClass(), $factoryHydrator->hydrate([], $resolvableInputObjectType)); 30 | 31 | $this->expectException(CannotHydrateException::class); 32 | $factoryHydrator->hydrate([], $badObjectType); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/InputTypeUtilsTest.php: -------------------------------------------------------------------------------- 1 | getInputTypeUtils(); 14 | 15 | $this->expectException(MissingTypeHintException::class); 16 | $this->expectExceptionMessage('Factory "TheCodingMachine\\GraphQL\\Controllers\\InputTypeUtilsTest::factoryNoReturnType" must have a return type.'); 17 | $inputTypeGenerator->getInputTypeNameAndClassName(new ReflectionMethod($this, 'factoryNoReturnType')); 18 | } 19 | 20 | public function testInvalidReturnType() 21 | { 22 | $inputTypeGenerator = $this->getInputTypeUtils(); 23 | 24 | $this->expectException(MissingTypeHintException::class); 25 | $this->expectExceptionMessage('The return type of factory "TheCodingMachine\\GraphQL\\Controllers\\InputTypeUtilsTest::factoryStringReturnType" must be an object, "string" passed instead.'); 26 | $inputTypeGenerator->getInputTypeNameAndClassName(new ReflectionMethod($this, 'factoryStringReturnType')); 27 | } 28 | 29 | public function testNullableReturnType() 30 | { 31 | $inputTypeGenerator = $this->getInputTypeUtils(); 32 | 33 | $this->expectException(MissingTypeHintException::class); 34 | $this->expectExceptionMessage('Factory "TheCodingMachine\\GraphQL\\Controllers\\InputTypeUtilsTest::factoryNullableReturnType" must have a non nullable return type.'); 35 | $inputTypeGenerator->getInputTypeNameAndClassName(new ReflectionMethod($this, 'factoryNullableReturnType')); 36 | } 37 | 38 | public function factoryNoReturnType() 39 | { 40 | 41 | } 42 | 43 | public function factoryStringReturnType(): string 44 | { 45 | return ''; 46 | } 47 | 48 | public function factoryNullableReturnType(): ?TestObject 49 | { 50 | return null; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /tests/Mappers/PorpaginasTypeMapperTest.php: -------------------------------------------------------------------------------- 1 | expectException(CannotMapTypeExceptionInterface::class); 19 | $mapper->mapClassToType("\stdClass", null, $this->getTypeMapper()); 20 | } 21 | 22 | public function testException2() 23 | { 24 | $mapper = new PorpaginasTypeMapper(); 25 | 26 | $this->expectException(RuntimeException::class); 27 | $mapper->mapClassToType(ArrayResult::class, new ListOfType(new StringType()), $this->getTypeMapper()); 28 | } 29 | 30 | public function testException3() 31 | { 32 | $mapper = new PorpaginasTypeMapper(); 33 | 34 | $this->expectException(CannotMapTypeExceptionInterface::class); 35 | $mapper->mapNameToType('foo', $this->getTypeMapper()); 36 | } 37 | 38 | public function testException4() 39 | { 40 | $mapper = new PorpaginasTypeMapper(); 41 | 42 | $this->expectException(CannotMapTypeExceptionInterface::class); 43 | $mapper->mapNameToType('PorpaginasResult_TestObjectInput', $this->getTypeMapper()); 44 | } 45 | 46 | public function testException5() 47 | { 48 | $mapper = new PorpaginasTypeMapper(); 49 | 50 | $this->expectException(CannotMapTypeExceptionInterface::class); 51 | $mapper->mapClassToInputType('foo', $this->getTypeMapper()); 52 | } 53 | 54 | public function testException6() 55 | { 56 | $mapper = new PorpaginasTypeMapper(); 57 | $type = new MutableObjectType(['name'=>'foo']); 58 | 59 | $this->expectException(CannotMapTypeExceptionInterface::class); 60 | $mapper->extendTypeForClass('foo', $type, $this->getTypeMapper()); 61 | } 62 | 63 | public function testException7() 64 | { 65 | $mapper = new PorpaginasTypeMapper(); 66 | $type = new MutableObjectType(['name'=>'foo']); 67 | 68 | $this->expectException(CannotMapTypeExceptionInterface::class); 69 | $mapper->extendTypeForName('foo', $type, $this->getTypeMapper()); 70 | } 71 | 72 | public function testCanMapClassToInputType() 73 | { 74 | $mapper = new PorpaginasTypeMapper(); 75 | 76 | $this->assertFalse($mapper->canMapClassToInputType('foo')); 77 | } 78 | 79 | public function testMapNameToType() 80 | { 81 | $mapper = new PorpaginasTypeMapper(); 82 | 83 | $type = $mapper->mapNameToType('PorpaginasResult_TestObject', $this->getTypeMapper()); 84 | 85 | $this->assertSame('PorpaginasResult_TestObject', $type->name); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/NamingStrategyTest.php: -------------------------------------------------------------------------------- 1 | assertSame('FooClassInput', $namingStrategy->getInputTypeName('Bar\\FooClass', $factory)); 17 | 18 | $factory = new Factory(['name'=>'MyInputType']); 19 | $this->assertSame('MyInputType', $namingStrategy->getInputTypeName('Bar\\FooClass', $factory)); 20 | } 21 | 22 | public function testGetFieldNameFromMethodName(): void 23 | { 24 | $namingStrategy = new NamingStrategy(); 25 | 26 | $this->assertSame('name', $namingStrategy->getFieldNameFromMethodName('getName')); 27 | $this->assertSame('get', $namingStrategy->getFieldNameFromMethodName('get')); 28 | $this->assertSame('name', $namingStrategy->getFieldNameFromMethodName('isName')); 29 | $this->assertSame('is', $namingStrategy->getFieldNameFromMethodName('is')); 30 | $this->assertSame('foo', $namingStrategy->getFieldNameFromMethodName('foo')); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/Reflection/CachedDocBlockFactoryTest.php: -------------------------------------------------------------------------------- 1 | getDocBlock($refMethod); 20 | $this->assertSame('Fetches a DocBlock object from a ReflectionMethod', $docBlock->getSummary()); 21 | $docBlock2 = $cachedDocBlockFactory->getDocBlock($refMethod); 22 | $this->assertSame($docBlock2, $docBlock); 23 | 24 | $newCachedDocBlockFactory = new CachedDocBlockFactory($arrayCache); 25 | $docBlock3 = $newCachedDocBlockFactory->getDocBlock($refMethod); 26 | $this->assertEquals($docBlock3, $docBlock); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/SchemaFactoryTest.php: -------------------------------------------------------------------------------- 1 | addControllerNamespace('TheCodingMachine\\GraphQL\\Controllers\\Fixtures\\Integration\\Controllers'); 28 | $factory->addTypeNamespace('TheCodingMachine\\GraphQL\\Controllers\\Fixtures\\Integration'); 29 | $factory->addQueryProvider(new AggregateQueryProvider([])); 30 | 31 | $schema = $factory->createSchema(); 32 | 33 | $this->doTestSchema($schema); 34 | } 35 | 36 | public function testSetters(): void 37 | { 38 | $container = new BasicAutoWiringContainer(new EmptyContainer()); 39 | $cache = new PhpFilesCache(); 40 | 41 | $factory = new SchemaFactory($cache, $container); 42 | 43 | $factory->addControllerNamespace('TheCodingMachine\\GraphQL\\Controllers\\Fixtures\\Integration\\Controllers'); 44 | $factory->addTypeNamespace('TheCodingMachine\\GraphQL\\Controllers\\Fixtures\\Integration'); 45 | $factory->setDoctrineAnnotationReader(new \Doctrine\Common\Annotations\AnnotationReader()) 46 | ->setHydrator(new FactoryHydrator()) 47 | ->setAuthenticationService(new VoidAuthenticationService()) 48 | ->setAuthorizationService(new VoidAuthorizationService()) 49 | ->setNamingStrategy(new NamingStrategy()) 50 | ->addTypeMapper(new CompositeTypeMapper([])) 51 | ->setSchemaConfig(new SchemaConfig()); 52 | 53 | $schema = $factory->createSchema(); 54 | 55 | $this->doTestSchema($schema); 56 | } 57 | 58 | public function testException(): void 59 | { 60 | $container = new BasicAutoWiringContainer(new EmptyContainer()); 61 | $cache = new PhpFilesCache(); 62 | 63 | $factory = new SchemaFactory($cache, $container); 64 | 65 | $this->expectException(GraphQLException::class); 66 | $factory->createSchema(); 67 | } 68 | 69 | public function testException2(): void 70 | { 71 | $container = new BasicAutoWiringContainer(new EmptyContainer()); 72 | $cache = new PhpFilesCache(); 73 | 74 | $factory = new SchemaFactory($cache, $container); 75 | $factory->addTypeNamespace('TheCodingMachine\\GraphQL\\Controllers\\Fixtures\\Integration'); 76 | 77 | $this->expectException(GraphQLException::class); 78 | $factory->createSchema(); 79 | } 80 | 81 | private function doTestSchema(Schema $schema): void 82 | { 83 | 84 | $schema->assertValid(); 85 | 86 | $queryString = ' 87 | query { 88 | contacts { 89 | name 90 | uppercaseName 91 | ... on User { 92 | email 93 | } 94 | } 95 | } 96 | '; 97 | 98 | $result = GraphQL::executeQuery( 99 | $schema, 100 | $queryString 101 | ); 102 | 103 | $this->assertSame([ 104 | 'contacts' => [ 105 | [ 106 | 'name' => 'Joe', 107 | 'uppercaseName' => 'JOE' 108 | ], 109 | [ 110 | 'name' => 'Bill', 111 | 'uppercaseName' => 'BILL', 112 | 'email' => 'bill@example.com' 113 | ] 114 | 115 | ] 116 | ], $result->toArray(Debug::RETHROW_INTERNAL_EXCEPTIONS)['data']); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/SchemaTest.php: -------------------------------------------------------------------------------- 1 | getTypeMapper(), $this->getTypeResolver()); 25 | 26 | $fields = $schema->getQueryType()->getFields(); 27 | $this->assertArrayHasKey('dummyQuery', $fields); 28 | $resolve = $fields['dummyQuery']->resolveFn; 29 | $this->assertSame('This is a placeholder query. Please create a query using the @Query annotation.', $resolve()); 30 | 31 | $fields = $schema->getMutationType()->getFields(); 32 | $this->assertArrayHasKey('dummyMutation', $fields); 33 | $resolve = $fields['dummyMutation']->resolveFn; 34 | $this->assertSame('This is a placeholder mutation. Please create a mutation using the @Mutation annotation.', $resolve()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Security/FailAuthenticationServiceTest.php: -------------------------------------------------------------------------------- 1 | expectException(SecurityNotImplementedException::class); 14 | $service->isLogged(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Security/FailAuthorizationServiceTest.php: -------------------------------------------------------------------------------- 1 | expectException(SecurityNotImplementedException::class); 14 | $service->isAllowed('foo'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/TypeGeneratorTest.php: -------------------------------------------------------------------------------- 1 | container = new Picotainer([ 17 | TypeFoo::class => function() { return new TypeFoo(); }, 18 | stdClass::class => function() { return new stdClass(); } 19 | ]); 20 | } 21 | 22 | public function testNameAndFields() 23 | { 24 | $typeGenerator = $this->getTypeGenerator(); 25 | 26 | $type = $typeGenerator->mapAnnotatedObject(TypeFoo::class, $this->getTypeMapper(), $this->container); 27 | 28 | $this->assertSame('TestObject', $type->name); 29 | $type->freeze(); 30 | $this->assertCount(1, $type->getFields()); 31 | } 32 | 33 | public function testMapAnnotatedObjectException() 34 | { 35 | $typeGenerator = $this->getTypeGenerator(); 36 | 37 | $this->expectException(MissingAnnotationException::class); 38 | $typeGenerator->mapAnnotatedObject(stdClass::class, $this->getTypeMapper(), $this->container); 39 | } 40 | 41 | public function testextendAnnotatedObjectException() 42 | { 43 | $typeGenerator = $this->getTypeGenerator(); 44 | 45 | $type = new MutableObjectType([ 46 | 'name' => 'foo', 47 | 'fields' => [] 48 | ]); 49 | 50 | $this->expectException(MissingAnnotationException::class); 51 | $typeGenerator->extendAnnotatedObject(new stdClass(), $type, $this->getTypeMapper()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/TypeRegistryTest.php: -------------------------------------------------------------------------------- 1 | 'Foo', 16 | 'fields' => function() {return [];} 17 | ]); 18 | 19 | $registry = new TypeRegistry(); 20 | $registry->registerType($type); 21 | 22 | $this->expectException(GraphQLException::class); 23 | $registry->registerType($type); 24 | } 25 | 26 | public function testGetType() 27 | { 28 | $type = new ObjectType([ 29 | 'name' => 'Foo', 30 | 'fields' => function() {return [];} 31 | ]); 32 | 33 | $registry = new TypeRegistry(); 34 | $registry->registerType($type); 35 | 36 | $this->assertSame($type, $registry->getType('Foo')); 37 | 38 | $this->expectException(GraphQLException::class); 39 | $registry->getType('Bar'); 40 | } 41 | 42 | public function testHasType() 43 | { 44 | $type = new ObjectType([ 45 | 'name' => 'Foo', 46 | 'fields' => function() {return [];} 47 | ]); 48 | 49 | $registry = new TypeRegistry(); 50 | $registry->registerType($type); 51 | 52 | $this->assertTrue($registry->hasType('Foo')); 53 | $this->assertFalse($registry->hasType('Bar')); 54 | 55 | } 56 | 57 | public function testGetMutableObjectType() 58 | { 59 | $type = new MutableObjectType([ 60 | 'name' => 'Foo', 61 | 'fields' => function() {return [];} 62 | ]); 63 | $type2 = new ObjectType([ 64 | 'name' => 'FooBar', 65 | 'fields' => function() {return [];} 66 | ]); 67 | 68 | $registry = new TypeRegistry(); 69 | $registry->registerType($type); 70 | $registry->registerType($type2); 71 | 72 | $this->assertSame($type, $registry->getMutableObjectType('Foo')); 73 | 74 | $this->expectException(GraphQLException::class); 75 | $this->assertSame($type, $registry->getMutableObjectType('FooBar')); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /tests/Types/IDTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidArgumentException::class); 14 | new ID(new stdClass()); 15 | } 16 | 17 | public function testVal() 18 | { 19 | $id = new ID(42); 20 | $this->assertSame(42, $id->val()); 21 | $this->assertSame('42', (string) $id); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Types/MutableObjectTypeTest.php: -------------------------------------------------------------------------------- 1 | type = new MutableObjectType([ 19 | 'name' => 'TestObject', 20 | 'fields' => [ 21 | 'test' => Type::string(), 22 | ], 23 | ]); 24 | } 25 | 26 | public function testGetStatus() 27 | { 28 | $this->assertSame(MutableObjectType::STATUS_PENDING, $this->type->getStatus()); 29 | $this->type->freeze(); 30 | $this->assertSame(MutableObjectType::STATUS_FROZEN, $this->type->getStatus()); 31 | } 32 | 33 | public function testAddFields() 34 | { 35 | $this->type->addFields(function() { 36 | return [ 37 | 'test' => Type::int(), 38 | 'test2' => Type::string(), 39 | ]; 40 | }); 41 | $this->type->addFields(function() { 42 | return [ 43 | 'test3' => Type::int(), 44 | ]; 45 | }); 46 | $this->type->freeze(); 47 | $fields = $this->type->getFields(); 48 | $this->assertCount(3, $fields); 49 | $this->assertArrayHasKey('test', $fields); 50 | $this->assertSame(Type::int(), $fields['test']->getType()); 51 | } 52 | 53 | public function testHasField() 54 | { 55 | $this->type->freeze(); 56 | $this->assertTrue($this->type->hasField('test')); 57 | } 58 | 59 | public function testGetField() 60 | { 61 | $this->type->freeze(); 62 | $this->assertSame(Type::string(), $this->type->getField('test')->getType()); 63 | } 64 | 65 | public function testHasFieldError() 66 | { 67 | $this->expectException(RuntimeException::class); 68 | $this->type->hasField('test'); 69 | } 70 | 71 | public function testGetFieldError() 72 | { 73 | $this->expectException(RuntimeException::class); 74 | $this->type->getField('test'); 75 | } 76 | 77 | public function testGetFieldsError() 78 | { 79 | $this->expectException(RuntimeException::class); 80 | $this->type->getFields(); 81 | } 82 | 83 | public function testAddFieldsError() 84 | { 85 | $this->type->freeze(); 86 | $this->expectException(RuntimeException::class); 87 | $this->type->addFields(function() {}); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/Types/ResolvableInputObjectTypeTest.php: -------------------------------------------------------------------------------- 1 | getControllerQueryProviderFactory(), 19 | $this->getTypeMapper(), 20 | new TestFactory(), 21 | 'myFactory', 22 | $this->getHydrator(), 23 | 'my comment'); 24 | 25 | $this->assertSame('InputObject', $inputType->name); 26 | $this->assertCount(2, $inputType->getFields()); 27 | $this->assertSame('my comment', $inputType->description); 28 | 29 | $obj = $inputType->resolve(['string' => 'foobar', 'bool' => false]); 30 | $this->assertInstanceOf(TestObject::class, $obj); 31 | $this->assertSame('foobar', $obj->getTest()); 32 | $this->assertSame(false, $obj->isTestBool()); 33 | 34 | $obj = $inputType->resolve(['string' => 'foobar']); 35 | $this->assertInstanceOf(TestObject::class, $obj); 36 | $this->assertSame('foobar', $obj->getTest()); 37 | $this->assertSame(true, $obj->isTestBool()); 38 | 39 | $this->expectException(GraphQLException::class); 40 | $this->expectExceptionMessage("Expected argument 'string' was not provided in GraphQL input type 'InputObject' used in factory 'TheCodingMachine\GraphQL\Controllers\Fixtures\Types\TestFactory::myFactory()'"); 41 | $inputType->resolve([]); 42 | } 43 | 44 | public function testListResolve(): void 45 | { 46 | $inputType = new ResolvableInputObjectType('InputObject2', 47 | $this->getControllerQueryProviderFactory(), 48 | $this->getTypeMapper(), 49 | new TestFactory(), 50 | 'myListFactory', 51 | $this->getHydrator(), 52 | null); 53 | 54 | $obj = $inputType->resolve(['date' => '2018-12-25', 'stringList' => 55 | [ 56 | 'foo', 57 | 'bar' 58 | ], 59 | 'dateList' => [ 60 | '2018-12-25' 61 | ]]); 62 | $this->assertInstanceOf(TestObject2::class, $obj); 63 | $this->assertSame('2018-12-25-foo-bar-1', $obj->getTest2()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /tests/Types/TypeResolverTest.php: -------------------------------------------------------------------------------- 1 | expectException(RuntimeException::class); 17 | $typeResolver->mapNameToType('ID'); 18 | } 19 | 20 | public function testMapNameToType() 21 | { 22 | $typeResolver = new TypeResolver(); 23 | $schema = new Schema([]); 24 | $typeResolver->registerSchema($schema); 25 | $this->assertInstanceOf(IDType::class, $typeResolver->mapNameToType('ID')); 26 | 27 | $this->expectException(CannotMapTypeException::class); 28 | $typeResolver->mapNameToType('NotExists'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Types/UnionTypeTest.php: -------------------------------------------------------------------------------- 1 | getTestObjectType(), $this->getTestObjectType2()], $this->getTypeMapper()); 17 | $resolveInfo = $this->getMockBuilder(ResolveInfo::class)->disableOriginalConstructor()->getMock(); 18 | $type = $unionType->resolveType(new TestObject('foo'), null, $resolveInfo); 19 | $this->assertSame($this->getTestObjectType(), $type); 20 | $type = $unionType->resolveType(new TestObject2('foo'), null, $resolveInfo); 21 | $this->assertSame($this->getTestObjectType2(), $type); 22 | } 23 | 24 | public function testException() 25 | { 26 | $unionType = new UnionType([$this->getTestObjectType(), $this->getTestObjectType2()], $this->getTypeMapper()); 27 | $this->expectException(\InvalidArgumentException::class); 28 | $resolveInfo = $this->getMockBuilder(ResolveInfo::class)->disableOriginalConstructor()->getMock(); 29 | $unionType->resolveType('foo', null, $resolveInfo); 30 | } 31 | 32 | public function testException2() 33 | { 34 | $this->expectException(\InvalidArgumentException::class); 35 | new UnionType([new StringType()], $this->getTypeMapper()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /website/core/Footer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const React = require('react'); 9 | 10 | class Footer extends React.Component { 11 | docUrl(doc, language) { 12 | const baseUrl = this.props.config.baseUrl; 13 | const docsUrl = this.props.config.docsUrl; 14 | const docsPart = `${docsUrl ? `${docsUrl}/` : ''}`; 15 | const langPart = `${language ? `${language}/` : ''}`; 16 | return `${baseUrl}${docsPart}${langPart}${doc}`; 17 | } 18 | 19 | pageUrl(doc, language) { 20 | const baseUrl = this.props.config.baseUrl; 21 | return baseUrl + (language ? `${language}/` : '') + doc; 22 | } 23 | 24 | render() { 25 | return ( 26 | 74 | ); 75 | } 76 | } 77 | 78 | module.exports = Footer; 79 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "examples": "docusaurus-examples", 4 | "start": "docusaurus-start", 5 | "build": "docusaurus-build", 6 | "publish-gh-pages": "docusaurus-publish", 7 | "write-translations": "docusaurus-write-translations", 8 | "version": "docusaurus-version", 9 | "rename-version": "docusaurus-rename-version" 10 | }, 11 | "devDependencies": { 12 | "docusaurus": "^1.6.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /website/pages/en/help.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const React = require('react'); 9 | 10 | const CompLibrary = require('../../core/CompLibrary.js'); 11 | 12 | const Container = CompLibrary.Container; 13 | const GridBlock = CompLibrary.GridBlock; 14 | 15 | function Help(props) { 16 | const {config: siteConfig, language = ''} = props; 17 | const {baseUrl, docsUrl} = siteConfig; 18 | const docsPart = `${docsUrl ? `${docsUrl}/` : ''}`; 19 | const langPart = `${language ? `${language}/` : ''}`; 20 | const docUrl = doc => `${baseUrl}${docsPart}${langPart}${doc}`; 21 | 22 | const supportLinks = [ 23 | { 24 | content: `Learn more using the [documentation on this site.](${docUrl( 25 | 'doc1.html', 26 | )})`, 27 | title: 'Browse Docs', 28 | }, 29 | { 30 | content: 'Ask questions about the documentation and project', 31 | title: 'Join the community', 32 | }, 33 | { 34 | content: "Find out what's new with this project", 35 | title: 'Stay up to date', 36 | }, 37 | ]; 38 | 39 | return ( 40 |
41 | 42 |
43 |
44 |

Need help?

45 |
46 |

This project is maintained by a dedicated group of people.

47 | 48 |
49 |
50 |
51 | ); 52 | } 53 | 54 | module.exports = Help; 55 | -------------------------------------------------------------------------------- /website/pages/en/users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const React = require('react'); 9 | 10 | const CompLibrary = require('../../core/CompLibrary.js'); 11 | 12 | const Container = CompLibrary.Container; 13 | 14 | class Users extends React.Component { 15 | render() { 16 | const {config: siteConfig} = this.props; 17 | if ((siteConfig.users || []).length === 0) { 18 | return null; 19 | } 20 | 21 | const editUrl = `${siteConfig.repoUrl}/edit/master/website/siteConfig.js`; 22 | const showcase = siteConfig.users.map(user => ( 23 | 24 | {user.caption} 25 | 26 | )); 27 | 28 | return ( 29 |
30 | 31 |
32 |
33 |

Who is Using This?

34 |

This project is used by many folks

35 |
36 |
{showcase}
37 |

Are you using this project?

38 | 39 | Add your company 40 | 41 |
42 |
43 |
44 | ); 45 | } 46 | } 47 | 48 | module.exports = Users; 49 | -------------------------------------------------------------------------------- /website/sidebars.json: -------------------------------------------------------------------------------- 1 | { 2 | "docs": { 3 | "Installation": ["getting-started", "symfony-bundle", "other-frameworks"], 4 | "Usage": ["my-first-query", "mutations", "type_mapping", "extend_type", "authentication_authorization", "external_type_declaration", "input-types", "inheritance"], 5 | "Advanced": ["file-uploads", "custom-output-types", "troubleshooting"], 6 | "Reference": ["annotations_reference"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /website/siteConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // See https://docusaurus.io/docs/site-config for all the possible 9 | // site configuration options. 10 | 11 | // List of projects/orgs using your project for the users page. 12 | /*const users = [ 13 | { 14 | caption: 'User1', 15 | // You will need to prepend the image path with your baseUrl 16 | // if it is not '/', like: '/test-site/img/docusaurus.svg'. 17 | image: '/img/docusaurus.svg', 18 | infoLink: 'https://www.facebook.com', 19 | pinned: true, 20 | }, 21 | ];*/ 22 | 23 | const siteConfig = { 24 | title: 'GraphQL-Controllers', // Title for your website. 25 | tagline: 'GraphQL in PHP made easy', 26 | url: 'https://thecodingmachine.github.io/', // Your website URL 27 | baseUrl: '/graphql-controllers/', // Base URL for your project */ 28 | // For github.io type URLs, you would set the url and baseUrl like: 29 | // url: 'https://facebook.github.io', 30 | // baseUrl: '/test-site/', 31 | 32 | // Used for publishing and more 33 | projectName: 'graphql-controllers', 34 | organizationName: 'thecodingmachine', 35 | // For top-level user or org sites, the organization is still the same. 36 | // e.g., for the https://JoelMarcey.github.io site, it would be set like... 37 | // organizationName: 'JoelMarcey' 38 | 39 | // For no header links in the top nav bar -> headerLinks: [], 40 | headerLinks: [ 41 | {doc: 'getting-started', label: 'Docs'}, 42 | {href: 'https://github.com/thecodingmachine/graphql-controllers', label: 'GitHub'}, 43 | /*{doc: 'doc4', label: 'API'},*/ 44 | {page: 'help', label: 'Help'}, 45 | /*{blog: true, label: 'Blog'},*/ 46 | ], 47 | 48 | // If you have users set above, you add it here: 49 | //users, 50 | 51 | /* path to images for header/footer */ 52 | headerIcon: 'img/graphql-controllers.svg', 53 | footerIcon: 'img/graphql-controllers.svg', 54 | favicon: 'img/logo.svg', 55 | 56 | /* Colors for website */ 57 | colors: { 58 | primaryColor: '#ef4136', 59 | secondaryColor: '#428bca', 60 | }, 61 | 62 | /* Custom fonts for website */ 63 | /* 64 | fonts: { 65 | myFont: [ 66 | "Times New Roman", 67 | "Serif" 68 | ], 69 | myOtherFont: [ 70 | "-apple-system", 71 | "system-ui" 72 | ] 73 | }, 74 | */ 75 | 76 | // This copyright info is used in /core/Footer.js and blog RSS/Atom feeds. 77 | copyright: `Copyright © 2017-${new Date().getFullYear()} TheCodingMachine`, 78 | 79 | highlight: { 80 | // Highlight.js theme to use for syntax highlighting in code blocks. 81 | theme: 'darcula', 82 | }, 83 | 84 | // Add custom scripts here that would be placed in