├── .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 | [](https://packagist.org/packages/thecodingmachine/graphql-controllers)
2 | [](https://packagist.org/packages/thecodingmachine/graphql-controllers)
3 | [](https://packagist.org/packages/thecodingmachine/graphql-controllers)
4 | [](https://packagist.org/packages/thecodingmachine/graphql-controllers)
5 | [](https://scrutinizer-ci.com/g/thecodingmachine/graphql-controllers/?branch=master)
6 | [](https://travis-ci.org/thecodingmachine/graphql-controllers)
7 | [](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 |
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 |
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 |
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