From eed7f245ebbf65603d911e143bc3400896fdacc9 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Fri, 10 Jun 2022 23:18:29 -0400 Subject: [PATCH] Version 6.0 Release (#483) * Update docs for #466 * Upgrade to use node 18 for CI * Upgrade Docusaurus * Added README for docs development and versioning * Versioned docs for 6.0 release --- .github/workflows/doc_generation.yml | 2 +- website/README | 32 + website/docs/input-types.mdx | 43 +- website/docusaurus.config.js | 16 +- website/package.json | 9 +- .../versioned_docs/version-6.0/CHANGELOG.md | 123 ++++ website/versioned_docs/version-6.0/README.mdx | 120 +++ .../version-6.0/annotations-reference.md | 282 +++++++ .../version-6.0/argument-resolving.md | 164 +++++ .../authentication-authorization.mdx | 295 ++++++++ .../versioned_docs/version-6.0/autowiring.mdx | 171 +++++ .../version-6.0/custom-types.mdx | 271 +++++++ .../doctrine-annotations-attributes.mdx | 164 +++++ .../version-6.0/error-handling.mdx | 222 ++++++ .../version-6.0/extend-input-type.mdx | 136 ++++ .../version-6.0/extend-type.mdx | 269 +++++++ .../version-6.0/external-type-declaration.mdx | 295 ++++++++ .../version-6.0/field-middlewares.md | 141 ++++ .../version-6.0/file-uploads.mdx | 91 +++ .../version-6.0/fine-grained-security.mdx | 421 +++++++++++ .../version-6.0/getting-started.md | 16 + .../version-6.0/implementing-security.md | 57 ++ .../version-6.0/inheritance-interfaces.mdx | 312 ++++++++ .../version-6.0/input-types.mdx | 696 ++++++++++++++++++ .../versioned_docs/version-6.0/internals.md | 142 ++++ .../version-6.0/laravel-package-advanced.mdx | 330 +++++++++ .../version-6.0/laravel-package.md | 153 ++++ .../versioned_docs/version-6.0/migrating.md | 54 ++ .../version-6.0/multiple-output-types.mdx | 256 +++++++ .../versioned_docs/version-6.0/mutations.mdx | 60 ++ .../version-6.0/other-frameworks.mdx | 331 +++++++++ .../versioned_docs/version-6.0/pagination.mdx | 99 +++ .../version-6.0/prefetch-method.mdx | 201 +++++ .../versioned_docs/version-6.0/queries.mdx | 251 +++++++ .../versioned_docs/version-6.0/query-plan.mdx | 109 +++ website/versioned_docs/version-6.0/semver.md | 45 ++ .../version-6.0/symfony-bundle-advanced.mdx | 229 ++++++ .../version-6.0/symfony-bundle.md | 121 +++ .../version-6.0/troubleshooting.md | 25 + .../version-6.0/type-mapping.mdx | 667 +++++++++++++++++ .../universal-service-providers.md | 74 ++ .../versioned_docs/version-6.0/validation.mdx | 287 ++++++++ .../version-6.0-sidebars.json | 55 ++ website/versions.json | 1 + 44 files changed, 7818 insertions(+), 20 deletions(-) create mode 100644 website/README create mode 100644 website/versioned_docs/version-6.0/CHANGELOG.md create mode 100644 website/versioned_docs/version-6.0/README.mdx create mode 100644 website/versioned_docs/version-6.0/annotations-reference.md create mode 100644 website/versioned_docs/version-6.0/argument-resolving.md create mode 100644 website/versioned_docs/version-6.0/authentication-authorization.mdx create mode 100644 website/versioned_docs/version-6.0/autowiring.mdx create mode 100644 website/versioned_docs/version-6.0/custom-types.mdx create mode 100644 website/versioned_docs/version-6.0/doctrine-annotations-attributes.mdx create mode 100644 website/versioned_docs/version-6.0/error-handling.mdx create mode 100644 website/versioned_docs/version-6.0/extend-input-type.mdx create mode 100644 website/versioned_docs/version-6.0/extend-type.mdx create mode 100644 website/versioned_docs/version-6.0/external-type-declaration.mdx create mode 100644 website/versioned_docs/version-6.0/field-middlewares.md create mode 100644 website/versioned_docs/version-6.0/file-uploads.mdx create mode 100644 website/versioned_docs/version-6.0/fine-grained-security.mdx create mode 100644 website/versioned_docs/version-6.0/getting-started.md create mode 100644 website/versioned_docs/version-6.0/implementing-security.md create mode 100644 website/versioned_docs/version-6.0/inheritance-interfaces.mdx create mode 100644 website/versioned_docs/version-6.0/input-types.mdx create mode 100644 website/versioned_docs/version-6.0/internals.md create mode 100644 website/versioned_docs/version-6.0/laravel-package-advanced.mdx create mode 100644 website/versioned_docs/version-6.0/laravel-package.md create mode 100644 website/versioned_docs/version-6.0/migrating.md create mode 100644 website/versioned_docs/version-6.0/multiple-output-types.mdx create mode 100644 website/versioned_docs/version-6.0/mutations.mdx create mode 100644 website/versioned_docs/version-6.0/other-frameworks.mdx create mode 100644 website/versioned_docs/version-6.0/pagination.mdx create mode 100644 website/versioned_docs/version-6.0/prefetch-method.mdx create mode 100644 website/versioned_docs/version-6.0/queries.mdx create mode 100644 website/versioned_docs/version-6.0/query-plan.mdx create mode 100644 website/versioned_docs/version-6.0/semver.md create mode 100644 website/versioned_docs/version-6.0/symfony-bundle-advanced.mdx create mode 100644 website/versioned_docs/version-6.0/symfony-bundle.md create mode 100644 website/versioned_docs/version-6.0/troubleshooting.md create mode 100644 website/versioned_docs/version-6.0/type-mapping.mdx create mode 100644 website/versioned_docs/version-6.0/universal-service-providers.md create mode 100644 website/versioned_docs/version-6.0/validation.mdx create mode 100644 website/versioned_sidebars/version-6.0-sidebars.json diff --git a/.github/workflows/doc_generation.yml b/.github/workflows/doc_generation.yml index b13e4cc0f1..e2774343b4 100644 --- a/.github/workflows/doc_generation.yml +++ b/.github/workflows/doc_generation.yml @@ -24,7 +24,7 @@ jobs: - name: "Setup NodeJS" uses: actions/setup-node@v3 with: - node-version: '14.x' + node-version: '18.x' - name: "Yarn install" run: yarn install diff --git a/website/README b/website/README new file mode 100644 index 0000000000..e1c4ebadb5 --- /dev/null +++ b/website/README @@ -0,0 +1,32 @@ +# GraphQLite Website + +The GraphQLite website is primarily a documentation site for GraphQLite. It's built on the Docusaurus framework, and is hosted on [GitHub](https://graphqlite.thecodingmachine.io/). + +More details on using Docusaurus can be found in the [official documentation](https://docusaurus.io/docs/docs-introduction). + +*This documentation could use further updating.* + +## Developing and Testing + +All code changes, aside from errors in previous documentation, should be done in the `docs` directory. The `package.json` file outlines available commands you can use for starting the server, building the docs, etc. + +- `npm run build` *(Transpiles/builds `docs` into `build` directory)* +- `npm run start` *(Starts the local npm server)* + +## Versioning + +Firstly, doc versions are cached in the `versioned_docs` directory. Meanwhile, the "Next" version, as represented on the website, is the current `master` branch of the `docs` dir. A list of the versions available is actually managed by the `versions.json` file, regardless of the `versioned_docs` directory. + +The [versioning section of the Docusaurus documentation](https://docusaurus.io/docs/versioning) covers this is more detail, but the following is related to our specific implementation: + +### Steps to Create and Deploy a new Version + +*All command paths are relative to the `website` directory. Ensure that you have `./node_modules/.bin` added to your path.* + +1. Firstly, we need to tag a new version, which will cache a copy of the documentation in the `versioned_docs` directory, update the `versioned_sidebars`, and update the `versions.json` file. + + ```bash + $ docusaurus docs:version 1.1 + ``` + *Be sure to include the X.X in the version, not just X.* +2. Technically, you're done, just commit these changes and the CI workflow will deploy when merged into `master`. diff --git a/website/docs/input-types.mdx b/website/docs/input-types.mdx index 1b27e0bfc7..f2c62afd40 100644 --- a/website/docs/input-types.mdx +++ b/website/docs/input-types.mdx @@ -128,6 +128,9 @@ Using the `#[Input]` attribute, we can transform the `Location` class, in the ex class Location { + #[Field] + private ?string $name = null; + #[Field] private float $latitude; @@ -140,6 +143,11 @@ class Location $this->longitude = $longitude; } + public function setName(string $name): void + { + $this->name = $name; + } + public function getLatitude(): float { return $this->latitude; @@ -162,6 +170,12 @@ class Location class Location { + /** + * @Field + * @var string|null + */ + private ?string $name = null; + /** * @Field * @var float @@ -180,6 +194,11 @@ class Location $this->longitude = $longitude; } + public function setName(string $name): void + { + $this->name = $name; + } + public function getLatitude(): float { return $this->latitude; @@ -199,13 +218,13 @@ Now if you call the `getCities` query, from the controller in the first example, There are some important things to notice: -- The `@Field` annotation is recognized only on properties for Input Type, not methods. +- The `@Field` annotation is recognized on properties for Input Type, as well as setters. - There are 3 ways for fields to be resolved: - Via constructor if corresponding properties are mentioned as parameters with the same names - exactly as in the example above. - If properties are public, they will be just set without any additional effort - no constructor required. - - For private or protected properties implemented, a public setter is required (if they are not set via the constructor). For example `setLatitude(float $latitude)`. + - For private or protected properties implemented, a public setter is required (if they are not set via the constructor). For example `setLatitude(float $latitude)`. You can also put the `@Field` annotation on the setter, instead of the property, allowing you to have use many other attributes (`Security`, `Right`, `Autowire`, etc.). - For validation of these Input Types, see the [Custom InputType Validation section](validation#custom-inputtype-validation). -- We advise using the `#[Input]` attribute on DTO style input type objects and not directly on your model objects. Using it on your model objects can cause coupling in undesirable ways. +- It's advised to use the `#[Input]` attribute on DTO style input type objects and not directly on your model objects. Using it on your model objects can cause coupling in undesirable ways. ### Multiple Input Types from the same class @@ -239,8 +258,14 @@ class UserInput #[Field(for: 'UpdateUserInput', inputType: 'String')] public string $password; + protected ?int $age; + + #[Field] - public ?int $age; + public function setAge(?int $age): void + { + $this->age = $age; + } } ``` @@ -274,11 +299,17 @@ class UserInput */ public $password; + /** @var int|null */ + protected $age; + /** * @Field() - * @var int|null + * @param int|null $age */ - public $age; + public function setAge(?int $age): void + { + $this->age = $age; + } } ``` diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js index 29437a4a68..66775d9370 100644 --- a/website/docusaurus.config.js +++ b/website/docusaurus.config.js @@ -43,6 +43,9 @@ module.exports={ "blog": {}, "theme": { "customCss": "/src/css/custom.css" + }, + "gtag": { + "trackingID": "UA-10196481-8" } } ] @@ -82,13 +85,10 @@ module.exports={ "href": 'https://github.com/thecodingmachine/graphqlite', } }, - "algolia": { - "apiKey": "8fcce617e281864dc03c68d17f6206db", - "indexName": "graphqlite_thecodingmachine", - "algoliaOptions": {} - }, - "gtag": { - "trackingID": "UA-10196481-8" - } + // "algolia": { + // "apiKey": "8fcce617e281864dc03c68d17f6206db", + // "indexName": "graphqlite_thecodingmachine", + // "algoliaOptions": {} + // } } } diff --git a/website/package.json b/website/package.json index 7ff552123a..580c70b2fd 100644 --- a/website/package.json +++ b/website/package.json @@ -6,13 +6,12 @@ "swizzle": "docusaurus swizzle", "deploy": "bin/deploy-github" }, - "devDependencies": { - }, + "devDependencies": {}, "dependencies": { - "@docusaurus/core": "2.0.0-beta.14", - "@docusaurus/preset-classic": "2.0.0-beta.14", + "@docusaurus/core": "2.0.0-beta.21", + "@docusaurus/preset-classic": "2.0.0-beta.21", "clsx": "^1.1.1", - "mdx-mermaid": "^1.1.0", + "mdx-mermaid": "^1.2.3", "mermaid": "^8.12.0", "react": "^17.0.1", "react-dom": "^17.0.1" diff --git a/website/versioned_docs/version-6.0/CHANGELOG.md b/website/versioned_docs/version-6.0/CHANGELOG.md new file mode 100644 index 0000000000..2e98e0ac9d --- /dev/null +++ b/website/versioned_docs/version-6.0/CHANGELOG.md @@ -0,0 +1,123 @@ +--- +id: changelog +title: Changelog +sidebar_label: Changelog +--- + +## 5.0.0 + +#### Dependencies: + +- Upgraded to using version 14.9 of [webonyx/graphql-php](https://github.com/webonyx/graphql-php) + +## 4.3.0 + +#### Breaking change: + +- The method `setAnnotationCacheDir($directory)` has been removed from the `SchemaFactory`. The annotation + cache will use your `Psr\SimpleCache\CacheInterface` compliant cache handler set through the `SchemaFactory` + constructor. + +#### Minor changes: + +- Removed dependency for doctrine/cache and unified some of the cache layers following a PSR interface. +- Cleaned up some of the documentation in an attempt to get things accurate with versioned releases. + +## 4.2.0 + +#### Breaking change: + +The method signature for `toGraphQLOutputType` and `toGraphQLInputType` have been changed to the following: + +```php +/** + * @param \ReflectionMethod|\ReflectionProperty $reflector + */ +public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector, DocBlock $docBlockObj): OutputType; + +/** + * @param \ReflectionMethod|\ReflectionProperty $reflector + */ +public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, $reflector, DocBlock $docBlockObj): InputType; +``` + +#### New features: + +- [@Input](annotations-reference.md#input-annotation) annotation is introduced as an alternative to `@Factory`. Now GraphQL input type can be created in the same manner as `@Type` in combination with `@Field` - [example](input-types.mdx#input-attribute). +- New attributes has been added to [@Field](annotations-reference.md#field-annotation) annotation: `for`, `inputType` and `description`. +- The following annotations now can be applied to class properties directly: `@Field`, `@Logged`, `@Right`, `@FailWith`, `@HideIfUnauthorized` and `@Security`. + +## 4.1.0 + +#### Breaking change: + +There is one breaking change introduced in the minor version (this was important to allow PHP 8 compatibility). + +- The **ecodev/graphql-upload** package (used to get support for file uploads in GraphQL input types) is now a "recommended" dependency only. + If you are using GraphQL file uploads, you need to add `ecodev/graphql-upload` to your `composer.json`. + +#### New features: + +- All annotations can now be accessed as PHP 8 attributes +- The `@deprecated` annotation in your PHP code translates into deprecated fields in your GraphQL schema +- You can now specify the GraphQL name of the Enum types you define +- Added the possibility to inject pure Webonyx objects in GraphQLite schema + +#### Minor changes: + +- Migrated from `zend/diactoros` to `laminas/diactoros` +- Making the annotation cache directory configurable + +#### Miscellaneous: + +- Migrated from Travis to Github actions + + +## 4.0.0 + +This is a complete refactoring from 3.x. While existing annotations are kept compatible, the internals have completely +changed. + +#### New features: + +- You can directly [annotate a PHP interface with `@Type` to make it a GraphQL interface](inheritance-interfaces.mdx#mapping-interfaces) +- You can autowire services in resolvers, thanks to the new `@Autowire` annotation +- Added [user input validation](validation.mdx) (using the Symfony Validator or the Laravel validator or a custom `@Assertion` annotation +- Improved security handling: + - Unauthorized access to fields can now generate GraphQL errors (rather that schema errors in GraphQLite v3) + - Added fine-grained security using the `@Security` annotation. A field can now be [marked accessible or not depending on the context](fine-grained-security.mdx). + For instance, you can restrict access to the field "viewsCount" of the type `BlogPost` only for post that the current user wrote. + - You can now inject the current logged user in any query / mutation / field using the `@InjectUser` annotation +- Performance: + - You can inject the [Webonyx query plan in a parameter from a resolver](query-plan.mdx) + - You can use the [dataloader pattern to improve performance drastically via the "prefetchMethod" attribute](prefetch-method.mdx) +- Customizable error handling has been added: + - You can throw [many errors in one exception](error-handling.mdx#many-errors-for-one-exception) with `TheCodingMachine\GraphQLite\Exceptions\GraphQLAggregateException` +- You can force input types using `@UseInputType(for="$id", inputType="ID!")` +- You can extend an input types (just like you could extend an output type in v3) using [the new `@Decorate` annotation](extend-input-type.mdx) +- In a factory, you can [exclude some optional parameters from the GraphQL schema](input-types#ignoring-some-parameters) + + +Many extension points have been added + +- Added a "root type mapper" (useful to map scalar types to PHP types or to add custom annotations related to resolvers) +- Added ["field middlewares"](field-middlewares.md) (useful to add middleware that modify the way GraphQL fields are handled) +- Added a ["parameter type mapper"](argument-resolving.md) (useful to add customize parameter resolution or add custom annotations related to parameters) + +New framework specific features: + +#### Symfony: + +- The Symfony bundle now provides a "login" and a "logout" mutation (and also a "me" query) + +#### Laravel: + +- [Native integration with the Laravel paginator](laravel-package-advanced.mdx#support-for-pagination) has been added + +#### Internals: + +- The `FieldsBuilder` class has been split in many different services (`FieldsBuilder`, `TypeHandler`, and a + chain of *root type mappers*) +- The `FieldsBuilderFactory` class has been completely removed. +- Overall, there is not much in common internally between 4.x and 3.x. 4.x is much more flexible with many more hook points + than 3.x. Try it out! diff --git a/website/versioned_docs/version-6.0/README.mdx b/website/versioned_docs/version-6.0/README.mdx new file mode 100644 index 0000000000..5ac2ccb123 --- /dev/null +++ b/website/versioned_docs/version-6.0/README.mdx @@ -0,0 +1,120 @@ +--- +id: index +title: GraphQLite +slug: / +sidebar_label: GraphQLite +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +

+ GraphQLite logo +

+ + +A PHP library that allows you to write your GraphQL queries in simple-to-write controllers. + +## Features + +* Create a complete GraphQL API by simply annotating your PHP classes +* Framework agnostic, but Symfony, Laravel and PSR-15 bindings available! +* Comes with batteries included: queries, mutations, mapping of arrays / iterators, file uploads, security, validation, extendable types and more! + +## Basic example + +First, declare a query in your controller: + + + + +```php +class ProductController +{ + #[Query] + public function product(string $id): Product + { + // Some code that looks for a product and returns it. + } +} +``` + + + + +```php +class ProductController +{ + /** + * @Query() + */ + public function product(string $id): Product + { + // Some code that looks for a product and returns it. + } +} +``` + + + + +Then, annotate the `Product` class to declare what fields are exposed to the GraphQL API: + + + + +```php +#[Type] +class Product +{ + #[Field] + public function getName(): string + { + return $this->name; + } + // ... +} +``` + + + + +```php +/** + * @Type() + */ +class Product +{ + /** + * @Field() + */ + public function getName(): string + { + return $this->name; + } + // ... +} +``` + + + + +That's it, you're good to go! Query and enjoy! + +```graphql +{ + product(id: 42) { + name + } +} +``` diff --git a/website/versioned_docs/version-6.0/annotations-reference.md b/website/versioned_docs/version-6.0/annotations-reference.md new file mode 100644 index 0000000000..e92a43357f --- /dev/null +++ b/website/versioned_docs/version-6.0/annotations-reference.md @@ -0,0 +1,282 @@ +--- +id: annotations-reference +title: Annotations reference +sidebar_label: Annotations reference +--- + +Note: all annotations are available both in a Doctrine annotation format (`@Query`) and in PHP 8 attribute format (`#[Query]`). +See [Doctrine annotations vs PHP 8 attributes](doctrine-annotations-attributes.mdx) for more details. + +## @Query + +The `@Query` annotation is used to declare a GraphQL query. + +**Applies on**: controller methods. + +Attribute | Compulsory | Type | Definition +---------------|------------|------|-------- +name | *no* | string | The name of the query. If skipped, the name of the method is used instead. +[outputType](custom-types.mdx) | *no* | string | Forces the GraphQL output type of a query. + +## @Mutation + +The `@Mutation` annotation is used to declare a GraphQL mutation. + +**Applies on**: controller methods. + +Attribute | Compulsory | Type | Definition +---------------|------------|------|-------- +name | *no* | string | The name of the mutation. If skipped, the name of the method is used instead. +[outputType](custom-types.mdx) | *no* | string | Forces the GraphQL output type of a query. + +## @Type + +The `@Type` annotation is used to declare a GraphQL object type. This is used with standard output +types, as well as enum types. For input types, use the [@Input annotation](#input-annotation) directly on the input type or a [@Factory annoation](#factory-annotation) to make/return an input type. + +**Applies on**: classes. + +Attribute | Compulsory | Type | Definition +---------------|------------|------|-------- +class | *no* | string | The targeted class/enum for the actual type. If no "class" attribute is passed, the type applies to the current class/enum. The current class/enum is assumed to be an entity (not service). If the "class" attribute *is passed*, [the class/enum annotated with `@Type` becomes a service](external-type-declaration.mdx). +name | *no* | string | The name of the GraphQL type generated. If not passed, the name of the class is used. If the class ends with "Type", the "Type" suffix is removed +default | *no* | bool | Defaults to *true*. Whether the targeted PHP class should be mapped by default to this type. +external | *no* | bool | Whether this is an [external type declaration](external-type-declaration.mdx) or not. You usually do not need to use this attribute since this value defaults to true if a "class" attribute is set. This is only useful if you are declaring a type with no PHP class mapping using the "name" attribute. + +## @ExtendType + +The `@ExtendType` annotation is used to add fields to an existing GraphQL object type. + +**Applies on**: classes. + +Attribute | Compulsory | Type | Definition +---------------|------------|------|-------- +class | see below | string | The targeted class. [The class annotated with `@ExtendType` is a service](extend-type.mdx). +name | see below | string | The targeted GraphQL output type. + +One and only one of "class" and "name" parameter can be passed at the same time. + +## @Input + +The `@Input` annotation is used to declare a GraphQL input type. + +**Applies on**: classes. + +Attribute | Compulsory | Type | Definition +---------------|------------|--------|-------- +name | *no* | string | The name of the GraphQL input type generated. If not passed, the name of the class with suffix "Input" is used. If the class ends with "Input", the "Input" suffix is not added. +description | *no* | string | Description of the input type in the documentation. If not passed, PHP doc comment is used. +default | *no* | bool | Name of the input type represented in your GraphQL schema. Defaults to `true` *only if* the name is not specified. If `name` is specified, this will default to `false`, so must also be included for `true` when `name` is used. +update | *no* | bool | Determines if the the input represents a partial update. When set to `true` all input fields will become optional and won't have default values thus won't be set on resolve if they are not specified in the query/mutation. This primarily applies to nullable fields. + +## @Field + +The `@Field` annotation is used to declare a GraphQL field. + +**Applies on**: methods or properties of classes annotated with `@Type`, `@ExtendType` or `@Input`. +When it's applied on private or protected property, public getter or/and setter method is expected in the class accordingly +whether it's used for output type or input type. For example if property name is `foo` then getter should be `getFoo()` or setter should be `setFoo($foo)`. Setter can be omitted if property related to the field is present in the constructor with the same name. + +Attribute | Compulsory | Type | Definition +------------------------------|------------|---------------|-------- +name | *no* | string | The name of the field. If skipped, the name of the method is used instead. +for | *no* | string, array | Forces the field to be used only for specific output or input type(s). By default field is used for all possible declared types. +description | *no* | string | Field description displayed in the GraphQL docs. If it's empty PHP doc comment is used instead. +[outputType](custom-types.mdx) | *no* | string | Forces the GraphQL output type of a query. +[inputType](input-types.mdx) | *no* | string | Forces the GraphQL input type of a query. + +## @SourceField + +The `@SourceField` annotation is used to declare a GraphQL field. + +**Applies on**: classes annotated with `@Type` or `@ExtendType`. + +Attribute | Compulsory | Type | Definition +---------------|------------|------|-------- +name | *yes* | string | The name of the field. +[outputType](custom-types.mdx) | *no* | string | Forces the GraphQL output type of the field. Otherwise, return type is used. +phpType | *no* | string | The PHP type of the field (as you would write it in a Docblock) +description | *no* | string | Field description displayed in the GraphQL docs. If it's empty PHP doc comment of the method in the source class is used instead. +sourceName | *no* | string | The name of the property in the source class. If not set, the `name` will be used to get property value. +annotations | *no* | array\ | A set of annotations that apply to this field. You would typically used a "@Logged" or "@Right" annotation here. Available in Doctrine annotations only (not available in the #SourceField PHP 8 attribute) + +**Note**: `outputType` and `phpType` are mutually exclusive. + +## @MagicField + +The `@MagicField` annotation is used to declare a GraphQL field that originates from a PHP magic property (using `__get` magic method). + +**Applies on**: classes annotated with `@Type` or `@ExtendType`. + +Attribute | Compulsory | Type | Definition +---------------|------------|------|-------- +name | *yes* | string | The name of the field. +[outputType](custom-types.mdx) | *no*(*) | string | The GraphQL output type of the field. +phpType | *no*(*) | string | The PHP type of the field (as you would write it in a Docblock) +description | *no* | string | Field description displayed in the GraphQL docs. If not set, no description will be shown. +sourceName | *no* | string | The name of the property in the source class. If not set, the `name` will be used to get property value. +annotations | *no* | array\ | A set of annotations that apply to this field. You would typically used a "@Logged" or "@Right" annotation here. Available in Doctrine annotations only (not available in the #MagicField PHP 8 attribute) + +(*) **Note**: `outputType` and `phpType` are mutually exclusive. You MUST provide one of them. + +## @Logged + +The `@Logged` annotation is used to declare a Query/Mutation/Field is only visible to logged users. + +**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field`. + +This annotation allows no attributes. + +## @Right + +The `@Right` annotation is used to declare a Query/Mutation/Field is only visible to users with a specific right. + +**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field`. + +Attribute | Compulsory | Type | Definition +---------------|------------|------|-------- +name | *yes* | string | The name of the right. + +## @FailWith + +The `@FailWith` annotation is used to declare a default value to return in the user is not authorized to see a specific +query / mutation / field (according to the `@Logged` and `@Right` annotations). + +**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field` and one of `@Logged` or `@Right` annotations. + +Attribute | Compulsory | Type | Definition +---------------|------------|------|-------- +value | *yes* | mixed | The value to return if the user is not authorized. + +## @HideIfUnauthorized + +The `@HideIfUnauthorized` annotation is used to completely hide the query / mutation / field if the user is not authorized +to access it (according to the `@Logged` and `@Right` annotations). + +**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field` and one of `@Logged` or `@Right` annotations. + +`@HideIfUnauthorized` and `@FailWith` are mutually exclusive. + +## @InjectUser + +Use the `@InjectUser` annotation to inject an instance of the current user logged in into a parameter of your +query / mutation / field. + +**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field`. + +Attribute | Compulsory | Type | Definition +---------------|------------|--------|-------- +*for* | *yes* | string | The name of the PHP parameter + +## @Security + +The `@Security` annotation can be used to check fin-grained access rights. +It is very flexible: it allows you to pass an expression that can contains custom logic. + +See [the fine grained security page](fine-grained-security.mdx) for more details. + +**Applies on**: methods or properties annotated with `@Query`, `@Mutation` or `@Field`. + +Attribute | Compulsory | Type | Definition +---------------|------------|--------|-------- +*default* | *yes* | string | The security expression + +## @Factory + +The `@Factory` annotation is used to declare a factory that turns GraphQL input types into objects. + +**Applies on**: methods from classes in the "types" namespace. + +Attribute | Compulsory | Type | Definition +---------------|------------|------|-------- +name | *no* | string | The name of the input type. If skipped, the name of class returned by the factory is used instead. +default | *no* | bool | If `true`, this factory will be used by default for its PHP return type. If set to `false`, you must explicitly [reference this factory using the `@Parameter` annotation](input-types.mdx#declaring-several-input-types-for-the-same-php-class). + +## @UseInputType + +Used to override the GraphQL input type of a PHP parameter. + +**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field` annotation. + +Attribute | Compulsory | Type | Definition +---------------|------------|------|-------- +*for* | *yes* | string | The name of the PHP parameter +*inputType* | *yes* | string | The GraphQL input type to force for this input field + +## @Decorate + +The `@Decorate` annotation is used [to extend/modify/decorate an input type declared with the `@Factory` annotation](extend-input-type.mdx). + +**Applies on**: methods from classes in the "types" namespace. + +Attribute | Compulsory | Type | Definition +---------------|------------|------|-------- +name | *yes* | string | The GraphQL input type name extended by this decorator. + +## @Autowire + +[Resolves a PHP parameter from the container](autowiring.mdx). + +Useful to inject services directly into `@Field` method arguments. + +**Applies on**: methods annotated with `@Query`, `@Mutation` or `@Field` annotation. + +Attribute | Compulsory | Type | Definition +---------------|------------|------|-------- +*for* | *yes* | string | The name of the PHP parameter +*identifier* | *no* | string | The identifier of the service to fetch. This is optional. Please avoid using this attribute as this leads to a "service locator" anti-pattern. + +## @HideParameter + +Removes [an argument from the GraphQL schema](input-types.mdx#ignoring-some-parameters). + +Attribute | Compulsory | Type | Definition +---------------|------------|------|-------- +*for* | *yes* | string | The name of the PHP parameter to hide + +## @Validate + +
This annotation is only available in the GraphQLite Laravel package
+ +[Validates a user input in Laravel](laravel-package-advanced.mdx). + +**Applies on**: methods annotated with `@Query`, `@Mutation`, `@Field`, `@Factory` or `@Decorator` annotation. + +Attribute | Compulsory | Type | Definition +---------------|------------|------|-------- +*for* | *yes* | string | The name of the PHP parameter +*rule* | *yes | string | Laravel validation rules + +Sample: + +```php +@Validate(for="$email", rule="email|unique:users") +``` + +## @Assertion + +[Validates a user input](validation.mdx). + +The `@Assertion` annotation is available in the *thecodingmachine/graphqlite-symfony-validator-bridge* third party package. +It is available out of the box if you use the Symfony bundle. + +**Applies on**: methods annotated with `@Query`, `@Mutation`, `@Field`, `@Factory` or `@Decorator` annotation. + +Attribute | Compulsory | Type | Definition +---------------|------------|------|-------- +*for* | *yes* | string | The name of the PHP parameter +*constraint* | *yes | annotation | One (or many) Symfony validation annotations. + +## ~~@EnumType~~ + +*Deprecated: Use [PHP 8.1's native Enums](https://www.php.net/manual/en/language.types.enumerations.php) instead with a [@Type](#type-annotation).* + +The `@EnumType` annotation is used to change the name of a "Enum" type. +Note that if you do not want to change the name, the annotation is optionnal. Any object extending `MyCLabs\Enum\Enum` +is automatically mapped to a GraphQL enum type. + +**Applies on**: classes extending the `MyCLabs\Enum\Enum` base class. + +Attribute | Compulsory | Type | Definition +---------------|------------|------|-------- +name | *no* | string | The name of the enum type (in the GraphQL schema) diff --git a/website/versioned_docs/version-6.0/argument-resolving.md b/website/versioned_docs/version-6.0/argument-resolving.md new file mode 100644 index 0000000000..35556c66d0 --- /dev/null +++ b/website/versioned_docs/version-6.0/argument-resolving.md @@ -0,0 +1,164 @@ +--- +id: argument-resolving +title: Extending argument resolving +sidebar_label: Custom argument resolving +--- +Available in GraphQLite 4.0+ + +Using a **parameter middleware**, you can hook into the argument resolution of field/query/mutation/factory. + +
Use a parameter middleware if you want to alter the way arguments are injected in a method +or if you want to alter the way input types are imported (for instance if you want to add a validation step)
+ +As an example, GraphQLite uses *parameter middlewares* internally to: + +- Inject the Webonyx GraphQL resolution object when you type-hint on the `ResolveInfo` object. For instance: + + ```php + /** + * @return Product[] + */ + #[Query] + public function products(ResolveInfo $info): array + ``` + + In the query above, the `$info` argument is filled with the Webonyx `ResolveInfo` class thanks to the + [`ResolveInfoParameterHandler parameter middleware`](https://github.com/thecodingmachine/graphqlite/blob/master/src/Mappers/Parameters/ResolveInfoParameterHandler.php) +- Inject a service from the container when you use the `@Autowire` annotation +- Perform validation with the `@Validate` annotation (in Laravel package) + + + +**Parameter middlewares** + + + +Each middleware is passed number of objects describing the parameter: + +- a PHP `ReflectionParameter` object representing the parameter being manipulated +- a `phpDocumentor\Reflection\DocBlock` instance (useful to analyze the `@param` comment if any) +- a `phpDocumentor\Reflection\Type` instance (useful to analyze the type if the argument) +- a `TheCodingMachine\GraphQLite\Annotations\ParameterAnnotations` instance. This is a collection of all custom annotations that apply to this specific argument (more on that later) +- a `$next` handler to pass the argument resolving to the next middleware. + +Parameter resolution is done in 2 passes. + +On the first pass, middlewares are traversed. They must return a `TheCodingMachine\GraphQLite\Parameters\ParameterInterface` (an object that does the actual resolving). + +```php +interface ParameterMiddlewareInterface +{ + public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, ?Type $paramTagType, ParameterAnnotations $parameterAnnotations, ParameterHandlerInterface $next): ParameterInterface; +} +``` + +Then, resolution actually happen by executing the resolver (this is the second pass). + +## Annotations parsing + +If you plan to use annotations while resolving arguments, your annotation should extend the [`ParameterAnnotationInterface`](https://github.com/thecodingmachine/graphqlite/blob/master/src/Annotations/ParameterAnnotationInterface.php) + +For instance, if we want GraphQLite to inject a service in an argument, we can use `@Autowire(for="myService")`. + +For PHP 8 attributes, we only need to put declare the annotation can target parameters: `#[Attribute(Attribute::TARGET_PARAMETER)]`. + +The annotation looks like this: + +```php +use Attribute; + +/** + * Use this annotation to autowire a service from the container into a given parameter of a field/query/mutation. + * + * @Annotation + */ +#[Attribute(Attribute::TARGET_PARAMETER)] +class Autowire implements ParameterAnnotationInterface +{ + /** + * @var string + */ + public $for; + + /** + * The getTarget method must return the name of the argument + */ + public function getTarget(): string + { + return $this->for; + } +} +``` + +## Writing the parameter middleware + +The middleware purpose is to analyze a parameter and decide whether or not it can handle it. + +```php title="Parameter middleware class" +class ContainerParameterHandler implements ParameterMiddlewareInterface +{ + /** @var ContainerInterface */ + private $container; + + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } + + public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, ?Type $paramTagType, ParameterAnnotations $parameterAnnotations, ParameterHandlerInterface $next): ParameterInterface + { + // The $parameterAnnotations object can be used to fetch any annotation implementing ParameterAnnotationInterface + $autowire = $parameterAnnotations->getAnnotationByType(Autowire::class); + + if ($autowire === null) { + // If there are no annotation, this middleware cannot handle the parameter. Let's ask + // the next middleware in the chain (using the $next object) + return $next->mapParameter($parameter, $docBlock, $paramTagType, $parameterAnnotations); + } + + // We found a @Autowire annotation, let's return a parameter resolver. + return new ContainerParameter($this->container, $parameter->getType()); + } +} +``` + +The last step is to write the actual parameter resolver. + +```php title="Parameter resolver class" +/** + * A parameter filled from the container. + */ +class ContainerParameter implements ParameterInterface +{ + /** @var ContainerInterface */ + private $container; + /** @var string */ + private $identifier; + + public function __construct(ContainerInterface $container, string $identifier) + { + $this->container = $container; + $this->identifier = $identifier; + } + + /** + * The "resolver" returns the actual value that will be fed to the function. + */ + public function resolve(?object $source, array $args, $context, ResolveInfo $info) + { + return $this->container->get($this->identifier); + } +} +``` + +## Registering a parameter middleware + +The last step is to register the parameter middleware we just wrote: + +You can register your own parameter middlewares using the `SchemaFactory::addParameterMiddleware()` method. + +```php +$schemaFactory->addParameterMiddleware(new ContainerParameterHandler($container)); +``` + +If you are using the Symfony bundle, you can tag the service as "graphql.parameter_middleware". diff --git a/website/versioned_docs/version-6.0/authentication-authorization.mdx b/website/versioned_docs/version-6.0/authentication-authorization.mdx new file mode 100644 index 0000000000..7e47280a82 --- /dev/null +++ b/website/versioned_docs/version-6.0/authentication-authorization.mdx @@ -0,0 +1,295 @@ +--- +id: authentication-authorization +title: Authentication and authorization +sidebar_label: Authentication and authorization +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +You might not want to expose your GraphQL API to anyone. Or you might want to keep some queries/mutations or fields +reserved to some users. + +GraphQLite offers some control over what a user can do with your API. You can restrict access to resources: + +- based on authentication using the [`@Logged` annotation](#logged-and-right-annotations) (restrict access to logged users) +- based on authorization using the [`@Right` annotation](#logged-and-right-annotations) (restrict access to logged users with certain rights). +- based on fine-grained authorization using the [`@Security` annotation](fine-grained-security.mdx) (restrict access for some given resources to some users). + +
+GraphQLite does not have its own security mechanism. +Unless you're using our Symfony Bundle or our Laravel package, it is up to you to connect this feature to your framework's security mechanism.
+See Connecting GraphQLite to your framework's security module. +
+ +## `@Logged` and `@Right` annotations + +GraphQLite exposes two annotations (`@Logged` and `@Right`) that you can use to restrict access to a resource. + + + +```php +namespace App\Controller; + +use TheCodingMachine\GraphQLite\Annotations\Query; +use TheCodingMachine\GraphQLite\Annotations\Logged; +use TheCodingMachine\GraphQLite\Annotations\Right; + +class UserController +{ + /** + * @return User[] + */ + #[Query] + #[Logged] + #[Right("CAN_VIEW_USER_LIST")] + public function users(int $limit, int $offset): array + { + // ... + } +} +``` + + + + +```php +namespace App\Controller; + +use TheCodingMachine\GraphQLite\Annotations\Query; +use TheCodingMachine\GraphQLite\Annotations\Logged; +use TheCodingMachine\GraphQLite\Annotations\Right; + +class UserController +{ + /** + * @Query + * @Logged + * @Right("CAN_VIEW_USER_LIST") + * @return User[] + */ + public function users(int $limit, int $offset): array + { + // ... + } +} +``` + + + + + +In the example above, the query `users` will only be available if the user making the query is logged AND if he +has the `CAN_VIEW_USER_LIST` right. + +`@Logged` and `@Right` annotations can be used next to: + +* `@Query` annotations +* `@Mutation` annotations +* `@Field` annotations + +
By default, if a user tries to access an unauthorized query/mutation/field, an error is raised and the query fails.
+ +## Not throwing errors + +If you do not want an error to be thrown when a user attempts to query a field/query/mutation he has no access to, you can use the `@FailWith` annotation. + +The `@FailWith` annotation contains the value that will be returned for users with insufficient rights. + + + + +```php +class UserController +{ + /** + * If a user is not logged or if the user has not the right "CAN_VIEW_USER_LIST", + * the value returned will be "null". + * + * @return User[] + */ + #[Query] + #[Logged] + #[Right("CAN_VIEW_USER_LIST")] + #[FailWith(value: null)] + public function users(int $limit, int $offset): array + { + // ... + } +} +``` + + + + +```php +class UserController +{ + /** + * If a user is not logged or if the user has not the right "CAN_VIEW_USER_LIST", + * the value returned will be "null". + * + * @Query + * @Logged + * @Right("CAN_VIEW_USER_LIST") + * @FailWith(null) + * @return User[] + */ + public function users(int $limit, int $offset): array + { + // ... + } +} +``` + + + + +## Injecting the current user as a parameter + +Use the `@InjectUser` annotation to get an instance of the current user logged in. + + + + +```php +namespace App\Controller; + +use TheCodingMachine\GraphQLite\Annotations\Query; +use TheCodingMachine\GraphQLite\Annotations\InjectUser; + +class ProductController +{ + /** + * @Query + * @return Product + */ + public function product( + int $id, + #[InjectUser] + User $user + ): Product + { + // ... + } +} +``` + + + + +```php +namespace App\Controller; + +use TheCodingMachine\GraphQLite\Annotations\Query; +use TheCodingMachine\GraphQLite\Annotations\InjectUser; + +class ProductController +{ + /** + * @Query + * @InjectUser(for="$user") + * @return Product + */ + public function product(int $id, User $user): Product + { + // ... + } +} +``` + + + + +The `@InjectUser` annotation can be used next to: + +* `@Query` annotations +* `@Mutation` annotations +* `@Field` annotations + +The object injected as the current user depends on your framework. It is in fact the object returned by the +["authentication service" configured in GraphQLite](implementing-security.md). + +## Hiding fields / queries / mutations + +By default, a user analysing the GraphQL schema can see all queries/mutations/types available. +Some will be available to him and some won't. + +If you want to add an extra level of security (or if you want your schema to be kept secret to unauthorized users), +you can use the `@HideIfUnauthorized` annotation. + + + + +```php +class UserController +{ + /** + * If a user is not logged or if the user has not the right "CAN_VIEW_USER_LIST", + * the schema will NOT contain the "users" query at all (so trying to call the + * "users" query will result in a GraphQL "query not found" error. + * + * @return User[] + */ + #[Query] + #[Logged] + #[Right("CAN_VIEW_USER_LIST")] + #[HideIfUnauthorized] + public function users(int $limit, int $offset): array + { + // ... + } +} +``` + + + + +```php +class UserController +{ + /** + * If a user is not logged or if the user has not the right "CAN_VIEW_USER_LIST", + * the schema will NOT contain the "users" query at all (so trying to call the + * "users" query will result in a GraphQL "query not found" error. + * + * @Query + * @Logged + * @Right("CAN_VIEW_USER_LIST") + * @HideIfUnauthorized() + * @return User[] + */ + public function users(int $limit, int $offset): array + { + // ... + } +} +``` + + + + +While this is the most secured mode, it can have drawbacks when working with development tools +(you need to be logged as admin to fetch the complete schema). + +
The "HideIfUnauthorized" mode was the default mode in GraphQLite 3 and is optionnal from GraphQLite 4+.
diff --git a/website/versioned_docs/version-6.0/autowiring.mdx b/website/versioned_docs/version-6.0/autowiring.mdx new file mode 100644 index 0000000000..c38bb73d77 --- /dev/null +++ b/website/versioned_docs/version-6.0/autowiring.mdx @@ -0,0 +1,171 @@ +--- +id: autowiring +title: Autowiring services +sidebar_label: Autowiring services +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +GraphQLite can automatically inject services in your fields/queries/mutations signatures. + +Some of your fields may be computed. In order to compute these fields, you might need to call a service. + +Most of the time, your `@Type` annotation will be put on a model. And models do not have access to services. +Hopefully, if you add a type-hinted service in your field's declaration, GraphQLite will automatically fill it with +the service instance. + +## Sample + +Let's assume you are running an international store. You have a `Product` class. Each product has many names (depending +on the language of the user). + + + + +```php +namespace App\Entities; + +use TheCodingMachine\GraphQLite\Annotations\Autowire; +use TheCodingMachine\GraphQLite\Annotations\Field; +use TheCodingMachine\GraphQLite\Annotations\Type; + +use Symfony\Component\Translation\TranslatorInterface; + +#[Type] +class Product +{ + // ... + + #[Field] + public function getName( + #[Autowire] + TranslatorInterface $translator + ): string + { + return $translator->trans('product_name_'.$this->id); + } +} +``` + + + + +```php +namespace App\Entities; + +use TheCodingMachine\GraphQLite\Annotations\Autowire; +use TheCodingMachine\GraphQLite\Annotations\Field; +use TheCodingMachine\GraphQLite\Annotations\Type; + +use Symfony\Component\Translation\TranslatorInterface; + +/** + * @Type() + */ +class Product +{ + // ... + + /** + * @Field() + * @Autowire(for="$translator") + */ + public function getName(TranslatorInterface $translator): string + { + return $translator->trans('product_name_'.$this->id); + } +} +``` + + + + +When GraphQLite queries the name, it will automatically fetch the translator service. + +
As with most autowiring solutions, GraphQLite assumes that the service identifier +in the container is the fully qualified class name of the type-hint. So in the example above, GraphQLite will +look for a service whose name is Symfony\Component\Translation\TranslatorInterface.
+ +## Best practices + +It is a good idea to refrain from type-hinting on concrete implementations. +Most often, your field declaration will be in your model. If you add a type-hint on a service, you are binding your domain +with a particular service implementation. This makes your code tightly coupled and less testable. + +
+Please don't do that: + +

+    #[Field]
+    public function getName(#[Autowire] MyTranslator $translator): string
+    {
+        // Your domain is suddenly tightly coupled to the MyTranslator class.
+    }
+
+
+ +Instead, be sure to type-hint against an interface. + +
+Do this instead: + +

+    #[Field]
+    public function getName(#[Autowire] TranslatorInterface $translator): string
+    {
+        // Good. You can switch translator implementation any time.
+    }
+
+
+ +By type-hinting against an interface, your code remains testable and is decoupled from the service implementation. + +## Fetching a service by name (discouraged!) + +Optionally, you can specify the identifier of the service you want to fetch from the controller: + + + + +```php +#[Autowire(identifier: "translator")] +``` + + + + +```php +/** + * @Autowire(for="$translator", identifier="translator") + */ +``` + + + + +
While GraphQLite offers the possibility to specify the name of the service to be +autowired, we would like to emphasize that this is highly discouraged. Hard-coding a container +identifier in the code of your class is akin to using the "service locator" pattern, which is known to be an +anti-pattern. Please refrain from doing this as much as possible.
+ +## Alternative solution + +You may find yourself uncomfortable with the autowiring mechanism of GraphQLite. For instance maybe: + +- Your service identifier in the container is not the fully qualified class name of the service (this is often true if you are not using a container supporting autowiring) +- You do not want to inject a service in a domain object +- You simply do not like the magic of injecting services in a method signature + +If you do not want to use autowiring and if you still need to access services to compute a field, please read on +the next chapter to learn [how to extend a type](extend-type). diff --git a/website/versioned_docs/version-6.0/custom-types.mdx b/website/versioned_docs/version-6.0/custom-types.mdx new file mode 100644 index 0000000000..34079ba2ab --- /dev/null +++ b/website/versioned_docs/version-6.0/custom-types.mdx @@ -0,0 +1,271 @@ +--- +id: custom-types +title: Custom types +sidebar_label: Custom types +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +In some special cases, you want to override the GraphQL return type that is attributed by default by GraphQLite. + +For instance: + + + + +```php +#[Type(class: Product::class)] +class ProductType +{ + #[Field] + public function getId(Product $source): string + { + return $source->getId(); + } +} +``` + + + + +```php +/** + * @Type(class=Product::class) + */ +class ProductType +{ + /** + * @Field + */ + public function getId(Product $source): string + { + return $source->getId(); + } +} +``` + + + + +In the example above, GraphQLite will generate a GraphQL schema with a field `id` of type `string`: + +```graphql +type Product { + id: String! +} +``` + +GraphQL comes with an `ID` scalar type. But PHP has no such type. So GraphQLite does not know when a variable +is an `ID` or not. + +You can help GraphQLite by manually specifying the output type to use: + + + + +```php + #[Field(outputType: "ID")] +``` + + + + +```php + /** + * @Field(name="id", outputType="ID") + */ +``` + + + + +## Usage + +The `outputType` attribute will map the return value of the method to the output type passed in parameter. + +You can use the `outputType` attribute in the following annotations: + +* `@Query` +* `@Mutation` +* `@Field` +* `@SourceField` +* `@MagicField` + +## Registering a custom output type (advanced) + +In order to create a custom output type, you need to: + +1. Design a class that extends `GraphQL\Type\Definition\ObjectType`. +2. Register this class in the GraphQL schema. + +You'll find more details on the [Webonyx documentation](https://webonyx.github.io/graphql-php/type-system/object-types/). + +--- + +In order to find existing types, the schema is using *type mappers* (classes implementing the `TypeMapperInterface` interface). + +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 +you use. + +### Symfony users + +Any class extending `GraphQL\Type\Definition\ObjectType` (and available in the container) will be automatically detected +by Symfony and added to the schema. + +If you want to automatically map the output type to a given PHP class, you will have to explicitly declare the output type +as a service and use the `graphql.output_type` tag: + +```yaml +# config/services.yaml +services: + App\MyOutputType: + tags: + - { name: 'graphql.output_type', class: 'App\MyPhpClass' } +``` + +### Other frameworks + +The easiest way is to use a `StaticTypeMapper`. Use this class to register custom output types. + +```php +// Sample code: +$staticTypeMapper = new StaticTypeMapper(); + +// Let's register a type that maps by default to the "MyClass" PHP class +$staticTypeMapper->setTypes([ + MyClass::class => new MyCustomOutputType() +]); + +// If you don't want your output type to map to any PHP class by default, use: +$staticTypeMapper->setNotMappedTypes([ + new MyCustomOutputType() +]); + +// Register the static type mapper in your application using the SchemaFactory instance +$schemaFactory->addTypeMapper($staticTypeMapper); +``` + +## Registering a custom scalar type (advanced) + +If you need to add custom scalar types, first, check the [GraphQLite Misc. Types library](https://github.com/thecodingmachine/graphqlite-misc-types). +It contains a number of "out-of-the-box" scalar types ready to use and you might find what you need there. + +You still need to develop your custom scalar type? Ok, let's get started. + +In order to add a scalar type in GraphQLite, you need to: + +- create a [Webonyx custom scalar type](https://webonyx.github.io/graphql-php/type-system/scalar-types/#writing-custom-scalar-types). + You do this by creating a class that extends `GraphQL\Type\Definition\ScalarType`. +- create a "type mapper" that will map PHP types to the GraphQL scalar type. You do this by writing a class implementing the `RootTypeMapperInterface`. +- create a "type mapper factory" that will be in charge of creating your "type mapper". + +```php +interface RootTypeMapperInterface +{ + /** + * @param \ReflectionMethod|\ReflectionProperty $reflector + */ + public function toGraphQLOutputType(Type $type, ?OutputType $subType, $reflector, DocBlock $docBlockObj): OutputType; + + /** + * @param \ReflectionMethod|\ReflectionProperty $reflector + */ + public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, $reflector, DocBlock $docBlockObj): InputType; + + public function mapNameToType(string $typeName): NamedType; +} +``` + +The `toGraphQLOutputType` and `toGraphQLInputType` are meant to map a return type (for output types) or a parameter type (for input types) +to your GraphQL scalar type. Return your scalar type if there is a match or `null` if there no match. + +The `mapNameToType` should return your GraphQL scalar type if `$typeName` is the name of your scalar type. + +RootTypeMapper are organized **in a chain** (they are actually middlewares). +Each instance of a `RootTypeMapper` holds a reference on the next root type mapper to be called in the chain. + +For instance: + +```php +class AnyScalarTypeMapper implements RootTypeMapperInterface +{ + /** @var RootTypeMapperInterface */ + private $next; + + public function __construct(RootTypeMapperInterface $next) + { + $this->next = $next; + } + + public function toGraphQLOutputType(Type $type, ?OutputType $subType, ReflectionMethod $refMethod, DocBlock $docBlockObj): ?OutputType + { + if ($type instanceof Scalar) { + // AnyScalarType is a class implementing the Webonyx ScalarType type. + return AnyScalarType::getInstance(); + } + // If the PHPDoc type is not "Scalar", let's pass the control to the next type mapper in the chain + return $this->next->toGraphQLOutputType($type, $subType, $refMethod, $docBlockObj); + } + + public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, ReflectionMethod $refMethod, DocBlock $docBlockObj): ?InputType + { + if ($type instanceof Scalar) { + // AnyScalarType is a class implementing the Webonyx ScalarType type. + return AnyScalarType::getInstance(); + } + // If the PHPDoc type is not "Scalar", let's pass the control to the next type mapper in the chain + return $this->next->toGraphQLInputType($type, $subType, $argumentName, $refMethod, $docBlockObj); + } + + /** + * Returns a GraphQL type by name. + * If this root type mapper can return this type in "toGraphQLOutputType" or "toGraphQLInputType", it should + * also map these types by name in the "mapNameToType" method. + * + * @param string $typeName The name of the GraphQL type + * @return NamedType|null + */ + public function mapNameToType(string $typeName): ?NamedType + { + if ($typeName === AnyScalarType::NAME) { + return AnyScalarType::getInstance(); + } + return null; + } +} +``` + +Now, in order to create an instance of your `AnyScalarTypeMapper` class, you need an instance of the `$next` type mapper in the chain. +How do you get the `$next` type mapper? Through a factory: + +```php +class AnyScalarTypeMapperFactory implements RootTypeMapperFactoryInterface +{ + public function create(RootTypeMapperInterface $next, RootTypeMapperFactoryContext $context): RootTypeMapperInterface + { + return new AnyScalarTypeMapper($next); + } +} +``` + +Now, you need to register this factory in your application, and we are done. + +You can register your own root mapper factories using the `SchemaFactory::addRootTypeMapperFactory()` method. + +```php +$schemaFactory->addRootTypeMapperFactory(new AnyScalarTypeMapperFactory()); +``` + +If you are using the Symfony bundle, the factory will be automatically registered, you have nothing to do (the service +is automatically tagged with the "graphql.root_type_mapper_factory" tag). diff --git a/website/versioned_docs/version-6.0/doctrine-annotations-attributes.mdx b/website/versioned_docs/version-6.0/doctrine-annotations-attributes.mdx new file mode 100644 index 0000000000..72725a39b2 --- /dev/null +++ b/website/versioned_docs/version-6.0/doctrine-annotations-attributes.mdx @@ -0,0 +1,164 @@ +--- +id: doctrine-annotations-attributes +title: Doctrine annotations VS PHP8 attributes +sidebar_label: Annotations VS Attributes +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +GraphQLite is heavily relying on the concept of annotations (also called attributes in PHP 8+). + +## Doctrine annotations + +
+ Deprecated! Doctrine annotations are deprecated in favor of native PHP 8 attributes. Support will be dropped in a future release. +
+ +Historically, attributes were not available in PHP and PHP developers had to "trick" PHP to get annotation support. This was the purpose of the [doctrine/annotation](https://www.doctrine-project.org/projects/doctrine-annotations/en/latest/index.html) library. + +Using Doctrine annotations, you write annotations in your docblocks: + +```php +use TheCodingMachine\GraphQLite\Annotations\Type; + +/** + * @Type + */ +class MyType +{ +} +``` + +Please note that: + +- The annotation is added in a **docblock** (a comment starting with "`/**`") +- The `Type` part is actually a class. It must be declared in the `use` statements at the top of your file. + + +
+ Heads up! +

Some IDEs provide support for Doctrine annotations:

+ + + We strongly recommend using an IDE that has Doctrine annotations support. +
+ +## PHP 8 attributes + +Starting with PHP 8, PHP got native annotations support. They are actually called "attributes" in the PHP world. + +The same code can be written this way: + +```php +use TheCodingMachine\GraphQLite\Annotations\Type; + +#[Type] +class MyType +{ +} +``` + +GraphQLite v4.1+ has support for PHP 8 attributes. + +The Doctrine annotation class and the PHP 8 attribute class is **the same** (so you will be using the same `use` statement at the top of your file). + +They support the same attributes too. + +A few notable differences: + +- PHP 8 attributes do not support nested attributes (unlike Doctrine annotations). This means there is no equivalent to the `annotations` attribute of `@MagicField` and `@SourceField`. +- PHP 8 attributes can be written at the parameter level. Any attribute targeting a "parameter" must be written at the parameter level. + +Let's take an example with the [`#Autowire` attribute](autowiring.mdx): + + + + +```php +#[Field] +public function getProduct(#[Autowire] ProductRepository $productRepository) : Product { + //... +} +``` + + + + +```php +/** + * @Field + * @Autowire(for="$productRepository") + */ +public function getProduct(ProductRepository $productRepository) : Product { + //... +} +``` + + + + + +## Migrating from Doctrine annotations to PHP 8 attributes + +The good news is that you can easily migrate from Doctrine annotations to PHP 8 attributes using the amazing, [Rector library](https://github.com/rectorphp/rector). To do so, you'll want to use the following rector configuration: + +```php title="rector.php" +import(SetList::CODE_QUALITY); + + // Set parameters + $parameters = $containerConfigurator->parameters(); + $parameters->set(Option::PATHS, [ + __DIR__ . '/src', + __DIR__ . '/tests', + ]); + + $services = $containerConfigurator->services(); + + // @Validate and @Assertion are part of other libraries, include if necessary + $services->set(AnnotationToAttributeRector::class) + ->configure([ + new AnnotationToAttribute(GraphQLite\Query::class), + new AnnotationToAttribute(GraphQLite\Mutation::class), + new AnnotationToAttribute(GraphQLite\Type::class), + new AnnotationToAttribute(GraphQLite\ExtendType::class), + new AnnotationToAttribute(GraphQLite\Input::class), + new AnnotationToAttribute(GraphQLite\Field::class), + new AnnotationToAttribute(GraphQLite\SourceField::class), + new AnnotationToAttribute(GraphQLite\MagicField::class), + new AnnotationToAttribute(GraphQLite\Logged::class), + new AnnotationToAttribute(GraphQLite\Right::class), + new AnnotationToAttribute(GraphQLite\FailWith::class), + new AnnotationToAttribute(GraphQLite\HideIfUnauthorized::class), + new AnnotationToAttribute(GraphQLite\InjectUser::class), + new AnnotationToAttribute(GraphQLite\Security::class), + new AnnotationToAttribute(GraphQLite\Factory::class), + new AnnotationToAttribute(GraphQLite\UseInputType::class), + new AnnotationToAttribute(GraphQLite\Decorate::class), + new AnnotationToAttribute(GraphQLite\Autowire::class), + new AnnotationToAttribute(GraphQLite\HideParameter::class), + new AnnotationToAttribute(GraphQLite\EnumType::class), + ]); +}; +``` diff --git a/website/versioned_docs/version-6.0/error-handling.mdx b/website/versioned_docs/version-6.0/error-handling.mdx new file mode 100644 index 0000000000..231f07db72 --- /dev/null +++ b/website/versioned_docs/version-6.0/error-handling.mdx @@ -0,0 +1,222 @@ +--- +id: error-handling +title: Error handling +sidebar_label: Error handling +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +In GraphQL, when an error occurs, the server must add an "error" entry in the response. + +```json +{ + "errors": [ + { + "message": "Name for character with ID 1002 could not be fetched.", + "locations": [ { "line": 6, "column": 7 } ], + "path": [ "hero", "heroFriends", 1, "name" ], + "extensions": { + "category": "Exception" + } + } + ] +} +``` + +You can generate such errors with GraphQLite by throwing a `GraphQLException`. + +```php +use TheCodingMachine\GraphQLite\Exceptions\GraphQLException; + +throw new GraphQLException("Exception message"); +``` + +## HTTP response code + +By default, when you throw a `GraphQLException`, the HTTP status code will be 500. + +If your exception code is in the 4xx - 5xx range, the exception code will be used as an HTTP status code. + +```php +// This exception will generate a HTTP 404 status code +throw new GraphQLException("Not found", 404); +``` + +
GraphQL allows to have several errors for one request. If you have several +GraphQLException thrown for the same request, the HTTP status code used will be the highest one.
+ +## Customizing the category + +By default, GraphQLite adds a "category" entry in the "extensions section". You can customize the category with the +4th parameter of the constructor: + +```php +throw new GraphQLException("Not found", 404, null, "NOT_FOUND"); +``` + +will generate: + +```json +{ + "errors": [ + { + "message": "Not found", + "extensions": { + "category": "NOT_FOUND" + } + } + ] +} +``` + +## Customizing the extensions section + +You can customize the whole "extensions" section with the 5th parameter of the constructor: + +```php +throw new GraphQLException("Field required", 400, null, "VALIDATION", ['field' => 'name']); +``` + +will generate: + +```json +{ + "errors": [ + { + "message": "Field required", + "extensions": { + "category": "VALIDATION", + "field": "name" + } + } + ] +} +``` + +## Writing your own exceptions + +Rather that throwing the base `GraphQLException`, you should consider writing your own exception. + +Any exception that implements interface `TheCodingMachine\GraphQLite\Exceptions\GraphQLExceptionInterface` will be displayed +in the GraphQL "errors" section. + +```php +class ValidationException extends Exception implements GraphQLExceptionInterface +{ + /** + * Returns true when exception message is safe to be displayed to a client. + */ + public function isClientSafe(): bool + { + return true; + } + + /** + * Returns string describing a category of the error. + * + * Value "graphql" is reserved for errors produced by query parsing or validation, do not use it. + */ + public function getCategory(): string + { + return 'VALIDATION'; + } + + /** + * Returns the "extensions" object attached to the GraphQL error. + * + * @return array + */ + public function getExtensions(): array + { + return []; + } +} +``` + +## Many errors for one exception + +Sometimes, you need to display several errors in the response. But of course, at any given point in your code, you can +throw only one exception. + +If you want to display several exceptions, you can bundle these exceptions in a `GraphQLAggregateException` that you can +throw. + + + + +```php +use TheCodingMachine\GraphQLite\Exceptions\GraphQLAggregateException; + +#[Query] +public function createProduct(string $name, float $price): Product +{ + $exceptions = new GraphQLAggregateException(); + + if ($name === '') { + $exceptions->add(new GraphQLException('Name cannot be empty', 400, null, 'VALIDATION')); + } + if ($price <= 0) { + $exceptions->add(new GraphQLException('Price must be positive', 400, null, 'VALIDATION')); + } + + if ($exceptions->hasExceptions()) { + throw $exceptions; + } +} +``` + + + + +```php +use TheCodingMachine\GraphQLite\Exceptions\GraphQLAggregateException; + +/** + * @Query + */ +public function createProduct(string $name, float $price): Product +{ + $exceptions = new GraphQLAggregateException(); + + if ($name === '') { + $exceptions->add(new GraphQLException('Name cannot be empty', 400, null, 'VALIDATION')); + } + if ($price <= 0) { + $exceptions->add(new GraphQLException('Price must be positive', 400, null, 'VALIDATION')); + } + + if ($exceptions->hasExceptions()) { + throw $exceptions; + } +} +``` + + + + +## Webonyx exceptions + +GraphQLite is based on the wonderful webonyx/GraphQL-PHP library. Therefore, the Webonyx exception mechanism can +also be used in GraphQLite. This means you can throw a `GraphQL\Error\Error` exception or any exception implementing +[`GraphQL\Error\ClientAware` interface](http://webonyx.github.io/graphql-php/error-handling/#errors-in-graphql) + +Actually, the `TheCodingMachine\GraphQLite\Exceptions\GraphQLExceptionInterface` extends Webonyx's `ClientAware` interface. + +## Behaviour of exceptions that do not implement ClientAware + +If an exception that does not implement `ClientAware` is thrown, by default, GraphQLite will not catch it. + +The exception will propagate to your framework error handler/middleware that is in charge of displaying the classical error page. + +You can [change the underlying behaviour of Webonyx to catch any exception and turn them into GraphQL errors](http://webonyx.github.io/graphql-php/error-handling/#debugging-tools). +The way you adjust the error settings depends on the framework you are using ([Symfony](symfony-bundle.md), [Laravel](laravel-package.md)). + +
To be clear: we strongly discourage changing this setting. We strongly believe that the +default "RETHROW_UNSAFE_EXCEPTIONS" setting of Webonyx is the only sane setting (only putting in "errors" section exceptions +designed for GraphQL).
diff --git a/website/versioned_docs/version-6.0/extend-input-type.mdx b/website/versioned_docs/version-6.0/extend-input-type.mdx new file mode 100644 index 0000000000..767856e32f --- /dev/null +++ b/website/versioned_docs/version-6.0/extend-input-type.mdx @@ -0,0 +1,136 @@ +--- +id: extend-input-type +title: Extending an input type +sidebar_label: Extending an input type +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Available in GraphQLite 4.0+ + +
If you are not familiar with the @Factory tag, read first the "input types" guide.
+ +Fields exposed in a GraphQL input type do not need to be all part of the factory method. + +Just like with output type (that can be [extended using the `ExtendType` annotation](extend-type.mdx)), you can extend/modify +an input type using the `@Decorate` annotation. + +Use the `@Decorate` annotation to add additional fields to an input type that is already declared by a `@Factory` annotation, +or to modify the returned object. + +
+ The @Decorate annotation is very useful in scenarios where you cannot touch the @Factory method. + This can happen if the @Factory method is defined in a third-party library or if the @Factory method is part + of auto-generated code. +
+ +Let's assume you have a `Filter` class used as an input type. You most certainly have a `@Factory` to create the input type. + + + + +```php +class MyFactory +{ + #[Factory] + public function createFilter(string $name): Filter + { + // Let's assume you have a flexible 'Filter' class that can accept any kind of filter + $filter = new Filter(); + $filter->addFilter('name', $name); + return $filter; + } +} +``` + + + + +```php +class MyFactory +{ + /** + * @Factory() + */ + public function createFilter(string $name): Filter + { + // Let's assume you have a flexible 'Filter' class that can accept any kind of filter + $filter = new Filter(); + $filter->addFilter('name', $name); + return $filter; + } +} +``` + + + + +Assuming you **cannot** modify the code of this factory, you can still modify the GraphQL input type generated by +adding a "decorator" around the factory. + + + + +```php +class MyDecorator +{ + #[Decorate(inputTypeName: "FilterInput")] + public function addTypeFilter(Filter $filter, string $type): Filter + { + $filter->addFilter('type', $type); + return $filter; + } +} +``` + + + + +```php +class MyDecorator +{ + /** + * @Decorate(inputTypeName="FilterInput") + */ + public function addTypeFilter(Filter $filter, string $type): Filter + { + $filter->addFilter('type', $type); + return $filter; + } +} +``` + + + + +In the example above, the "Filter" input type is modified. We add an additional "type" field to the input type. + +A few things to notice: + +- The decorator takes the object generated by the factory as first argument +- The decorator MUST return an object of the same type (or a sub-type) +- The decorator CAN contain additional parameters. They will be added to the fields of the GraphQL input type. +- The `@Decorate` annotation must contain a `inputTypeName` attribute that contains the name of the GraphQL input type + that is decorated. If you did not specify this name in the `@Factory` annotation, this is by default the name of the + PHP class + "Input" (for instance: "Filter" => "FilterInput") + + +
+ Heads up! The MyDecorator class must exist in the container of your + application and the container identifier MUST be the fully qualified class name. +

+ If you are using the Symfony bundle (or a framework with autowiring like Laravel), this is usually + not an issue as the container will automatically create the controller entry if you do not explicitly + declare it. +
diff --git a/website/versioned_docs/version-6.0/extend-type.mdx b/website/versioned_docs/version-6.0/extend-type.mdx new file mode 100644 index 0000000000..c8efb8d920 --- /dev/null +++ b/website/versioned_docs/version-6.0/extend-type.mdx @@ -0,0 +1,269 @@ +--- +id: extend-type +title: Extending a type +sidebar_label: Extending a type +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Fields exposed in a GraphQL type do not need to be all part of the same class. + +Use the `@ExtendType` annotation to add additional fields to a type that is already declared. + +
+ Extending a type has nothing to do with type inheritance. + If you are looking for a way to expose a class and its children classes, have a look at + the Inheritance section +
+ +Let's assume you have a `Product` class. In order to get the name of a product, there is no `getName()` method in +the product because the name needs to be translated in the correct language. You have a `TranslationService` to do that. + + + + +```php +namespace App\Entities; + +use TheCodingMachine\GraphQLite\Annotations\Field; +use TheCodingMachine\GraphQLite\Annotations\Type; + +#[Type] +class Product +{ + // ... + + #[Field] + public function getId(): string + { + return $this->id; + } + + #[Field] + public function getPrice(): ?float + { + return $this->price; + } +} +``` + + + + +```php +namespace App\Entities; + +use TheCodingMachine\GraphQLite\Annotations\Field; +use TheCodingMachine\GraphQLite\Annotations\Type; + +/** + * @Type() + */ +class Product +{ + // ... + + /** + * @Field() + */ + public function getId(): string + { + return $this->id; + } + + /** + * @Field() + */ + public function getPrice(): ?float + { + return $this->price; + } +} +``` + + + + +```php +// You need to use a service to get the name of the product in the correct language. +$name = $translationService->getProductName($productId, $language); +``` + +Using `@ExtendType`, you can add an additional `name` field to your product: + + + + +```php +namespace App\Types; + +use TheCodingMachine\GraphQLite\Annotations\ExtendType; +use TheCodingMachine\GraphQLite\Annotations\Field; +use App\Entities\Product; + +#[ExtendType(class: Product::class)] +class ProductType +{ + private $translationService; + + public function __construct(TranslationServiceInterface $translationService) + { + $this->translationService = $translationService; + } + + #[Field] + public function getName(Product $product, string $language): string + { + return $this->translationService->getProductName($product->getId(), $language); + } +} +``` + + + + +```php +namespace App\Types; + +use TheCodingMachine\GraphQLite\Annotations\ExtendType; +use TheCodingMachine\GraphQLite\Annotations\Field; +use App\Entities\Product; + +/** + * @ExtendType(class=Product::class) + */ +class ProductType +{ + private $translationService; + + public function __construct(TranslationServiceInterface $translationService) + { + $this->translationService = $translationService; + } + + /** + * @Field() + */ + public function getName(Product $product, string $language): string + { + return $this->translationService->getProductName($product->getId(), $language); + } +} +``` + + + + +Let's break this sample: + + + + +```php +#[ExtendType(class: Product::class)] +``` + + + + +```php +/** + * @ExtendType(class=Product::class) + */ +``` + + + + +With the `@ExtendType` annotation, we tell GraphQLite that we want to add fields in the GraphQL type mapped to +the `Product` PHP class. + +```php +class ProductType +{ + private $translationService; + + public function __construct(TranslationServiceInterface $translationService) + { + $this->translationService = $translationService; + } + + // ... +} +``` + + +- The `ProductType` class must be in the types namespace. You configured this namespace when you installed GraphQLite. +- The `ProductType` class is actually a **service**. You can therefore inject dependencies in it (like the `$translationService` in this example) + +
Heads up! The ProductType class must exist in the container of your +application and the container identifier MUST be the fully qualified class name.

+If you are using the Symfony bundle (or a framework with autowiring like Laravel), this +is usually not an issue as the container will automatically create the controller entry if you do not explicitly +declare it.
+ + + + +```php +#[Field] +public function getName(Product $product, string $language): string +{ + return $this->translationService->getProductName($product->getId(), $language); +} +``` + + + + +```php +/** + * @Field() + */ +public function getName(Product $product, string $language): string +{ + return $this->translationService->getProductName($product->getId(), $language); +} +``` + + + + +The `@Field` annotation is used to add the "name" field to the `Product` type. + +Take a close look at the signature. The first parameter is the "resolved object" we are working on. +Any additional parameters are used as arguments. + +Using the "[Type language](https://graphql.org/learn/schema/#type-language)" notation, we defined a type extension for +the GraphQL "Product" type: + +```graphql +Extend type Product { + name(language: !String): String! +} +``` + +
Type extension is a very powerful tool. Use it to add fields that needs to be +computed from services not available in the entity. +
diff --git a/website/versioned_docs/version-6.0/external-type-declaration.mdx b/website/versioned_docs/version-6.0/external-type-declaration.mdx new file mode 100644 index 0000000000..d11cd9ad77 --- /dev/null +++ b/website/versioned_docs/version-6.0/external-type-declaration.mdx @@ -0,0 +1,295 @@ +--- +id: external-type-declaration +title: External type declaration +sidebar_label: External type declaration +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +In some cases, you cannot or do not want to put an annotation on a domain class. + +For instance: + +* The class you want to annotate is part of a third party library and you cannot modify it +* You are doing domain-driven design and don't want to clutter your domain object with annotations from the view layer +* etc. + +## `@Type` annotation with the `class` attribute + +GraphQLite allows you to use a *proxy* class thanks to the `@Type` annotation with the `class` attribute: + + + + +```php +namespace App\Types; + +use TheCodingMachine\GraphQLite\Annotations\Type; +use TheCodingMachine\GraphQLite\Annotations\Field; +use App\Entities\Product; + +#[Type(class: Product::class)] +class ProductType +{ + #[Field] + public function getId(Product $product): string + { + return $product->getId(); + } +} +``` + + + + +```php +namespace App\Types; + +use TheCodingMachine\GraphQLite\Annotations\Type; +use TheCodingMachine\GraphQLite\Annotations\Field; +use App\Entities\Product; + +/** + * @Type(class=Product::class) + */ +class ProductType +{ + /** + * @Field() + */ + public function getId(Product $product): string + { + return $product->getId(); + } +} +``` + + + + +The `ProductType` class must be in the *types* namespace. You configured this namespace when you installed GraphQLite. + +The `ProductType` class is actually a **service**. You can therefore inject dependencies in it. + +
Heads up! The ProductType class must exist in the container of your application and the container identifier MUST be the fully qualified class name.

+If you are using the Symfony bundle (or a framework with autowiring like Laravel), this +is usually not an issue as the container will automatically create the controller entry if you do not explicitly +declare it.
+ +In methods with a `@Field` annotation, the first parameter is the *resolved object* we are working on. Any additional parameters are used as arguments. + +## `@SourceField` annotation + +If you don't want to rewrite all *getters* of your base class, you may use the `@SourceField` annotation: + + + + +```php +use TheCodingMachine\GraphQLite\Annotations\Type; +use TheCodingMachine\GraphQLite\Annotations\SourceField; +use App\Entities\Product; + +#[Type(class: Product::class)] +#[SourceField(name: "name")] +#[SourceField(name: "price")] +class ProductType +{ +} +``` + + + + +```php +use TheCodingMachine\GraphQLite\Annotations\Type; +use TheCodingMachine\GraphQLite\Annotations\SourceField; +use App\Entities\Product; + +/** + * @Type(class=Product::class) + * @SourceField(name="name") + * @SourceField(name="price") + */ +class ProductType +{ +} +``` + + + + +By doing so, you let GraphQLite know that the type exposes the `getName` method of the underlying `Product` object. + +Internally, GraphQLite will look for methods named `name()`, `getName()` and `isName()`). +You can set different name to look for with `sourceName` attribute. + +## `@MagicField` annotation + +If your object has no getters, but instead uses magic properties (using the magic `__get` method), you should use the `@MagicField` annotation: + + + + +```php +use TheCodingMachine\GraphQLite\Annotations\Type; +use TheCodingMachine\GraphQLite\Annotations\SourceField; +use App\Entities\Product; + +#[Type] +#[MagicField(name: "name", outputType: "String!")] +#[MagicField(name: "price", outputType: "Float")] +class ProductType +{ + public function __get(string $property) { + // return some magic property + } +} +``` + + + + +```php +use TheCodingMachine\GraphQLite\Annotations\Type; +use TheCodingMachine\GraphQLite\Annotations\SourceField; +use App\Entities\Product; + +/** + * @Type() + * @MagicField(name="name", outputType="String!") + * @MagicField(name="price", outputType="Float") + */ +class ProductType +{ + public function __get(string $property) { + // return some magic property + } +} +``` + + + + +By doing so, you let GraphQLite know that the type exposes "name" and the "price" magic properties of the underlying `Product` object. +You can set different name to look for with `sourceName` attribute. + +This is particularly useful in frameworks like Laravel, where Eloquent is making a very wide use of such properties. + +Please note that GraphQLite has no way to know the type of a magic property. Therefore, you have specify the GraphQL type +of each property manually. + +### Authentication and authorization + +You may also check for logged users or users with a specific right using the "annotations" property. + +```php +use TheCodingMachine\GraphQLite\Annotations\Type; +use TheCodingMachine\GraphQLite\Annotations\SourceField; +use TheCodingMachine\GraphQLite\Annotations\Logged; +use TheCodingMachine\GraphQLite\Annotations\Right; +use TheCodingMachine\GraphQLite\Annotations\FailWith; +use App\Entities\Product; + +/** + * @Type(class=Product::class) + * @SourceField(name="name") + * @SourceField(name="price", annotations={@Logged, @Right(name="CAN_ACCESS_Price", @FailWith(null)})) + */ +class ProductType extends AbstractAnnotatedObjectType +{ +} +``` + +Any annotations described in the [Authentication and authorization page](authentication-authorization.mdx), or any annotation this is actually a ["field middleware"](field-middlewares.md) can be used in the `@SourceField` "annotations" attribute. + +
Heads up! The "annotation" attribute in @SourceField and @MagicField is only available as a Doctrine annotations. You cannot use it in PHP 8 attributes (because PHP 8 attributes cannot be nested)
+ +## Declaring fields dynamically (without annotations) + +In some very particular cases, you might not know exactly the list of `@SourceField` annotations at development time. +If you need to decide the list of `@SourceField` at runtime, you can implement the `FromSourceFieldsInterface`: + + + + +```php +use TheCodingMachine\GraphQLite\FromSourceFieldsInterface; + +#[Type(class: Product::class)] +class ProductType implements FromSourceFieldsInterface +{ + /** + * Dynamically returns the array of source fields + * to be fetched from the original object. + * + * @return SourceFieldInterface[] + */ + public function getSourceFields(): array + { + // You may want to enable fields conditionally based on feature flags... + if (ENABLE_STATUS_GLOBALLY) { + return [ + new SourceField(['name'=>'status', 'logged'=>true]), + ]; + } else { + return []; + } + } +} +``` + + + + +```php +use TheCodingMachine\GraphQLite\FromSourceFieldsInterface; + +/** + * @Type(class=Product::class) + */ +class ProductType implements FromSourceFieldsInterface +{ + /** + * Dynamically returns the array of source fields + * to be fetched from the original object. + * + * @return SourceFieldInterface[] + */ + public function getSourceFields(): array + { + // You may want to enable fields conditionally based on feature flags... + if (ENABLE_STATUS_GLOBALLY) { + return [ + new SourceField(['name'=>'status', 'logged'=>true]), + ]; + } else { + return []; + } + } +} +``` + + + diff --git a/website/versioned_docs/version-6.0/field-middlewares.md b/website/versioned_docs/version-6.0/field-middlewares.md new file mode 100644 index 0000000000..e04f196442 --- /dev/null +++ b/website/versioned_docs/version-6.0/field-middlewares.md @@ -0,0 +1,141 @@ +--- +id: field-middlewares +title: Adding custom annotations with Field middlewares +sidebar_label: Custom annotations +--- + +Available in GraphQLite 4.0+ + +Just like the `@Logged` or `@Right` annotation, you can develop your own annotation that extends/modifies the behaviour of a field/query/mutation. + +
+ If you want to create an annotation that targets a single argument (like @AutoWire(for="$service")), you should rather check the documentation about custom argument resolving +
+ +## Field middlewares + +GraphQLite is based on the Webonyx/Graphql-PHP library. In Webonyx, fields are represented by the `FieldDefinition` class. +In order to create a `FieldDefinition` instance for your field, GraphQLite goes through a series of "middlewares". + +![](/img/field_middleware.svg) + +Each middleware is passed a `TheCodingMachine\GraphQLite\QueryFieldDescriptor` instance. This object contains all the +parameters used to initialize the field (like the return type, the list of arguments, the resolver to be used, etc...) + +Each middleware must return a `GraphQL\Type\Definition\FieldDefinition` (the object representing a field in Webonyx/GraphQL-PHP). + +```php +/** + * Your middleware must implement this interface. + */ +interface FieldMiddlewareInterface +{ + public function process(QueryFieldDescriptor $queryFieldDescriptor, FieldHandlerInterface $fieldHandler): ?FieldDefinition; +} +``` + +```php +class QueryFieldDescriptor +{ + public function getName() { /* ... */ } + public function setName(string $name) { /* ... */ } + public function getType() { /* ... */ } + public function setType($type): void { /* ... */ } + public function getParameters(): array { /* ... */ } + public function setParameters(array $parameters): void { /* ... */ } + public function getPrefetchParameters(): array { /* ... */ } + public function setPrefetchParameters(array $prefetchParameters): void { /* ... */ } + public function getPrefetchMethodName(): ?string { /* ... */ } + public function setPrefetchMethodName(?string $prefetchMethodName): void { /* ... */ } + public function setCallable(callable $callable): void { /* ... */ } + public function setTargetMethodOnSource(?string $targetMethodOnSource): void { /* ... */ } + public function isInjectSource(): bool { /* ... */ } + public function setInjectSource(bool $injectSource): void { /* ... */ } + public function getComment(): ?string { /* ... */ } + public function setComment(?string $comment): void { /* ... */ } + public function getMiddlewareAnnotations(): MiddlewareAnnotations { /* ... */ } + public function setMiddlewareAnnotations(MiddlewareAnnotations $middlewareAnnotations): void { /* ... */ } + public function getOriginalResolver(): ResolverInterface { /* ... */ } + public function getResolver(): callable { /* ... */ } + public function setResolver(callable $resolver): void { /* ... */ } +} +``` + +The role of a middleware is to analyze the `QueryFieldDescriptor` and modify it (or to directly return a `FieldDefinition`). + +If you want the field to purely disappear, your middleware can return `null`. + +## Annotations parsing + +Take a look at the `QueryFieldDescriptor::getMiddlewareAnnotations()`. + +It returns the list of annotations applied to your field that implements the `MiddlewareAnnotationInterface`. + +Let's imagine you want to add a `@OnlyDebug` annotation that displays a field/query/mutation only in debug mode (and +hides the field in production). That could be useful, right? + +First, we have to define the annotation. Annotations are handled by the great [doctrine/annotations](https://www.doctrine-project.org/projects/doctrine-annotations/en/1.6/index.html) library (for PHP 7+) and/or by PHP 8 attributes. + +```php title="OnlyDebug.php" +namespace App\Annotations; + +use Attribute; +use TheCodingMachine\GraphQLite\Annotations\MiddlewareAnnotationInterface; + +/** + * @Annotation + * @Target({"METHOD", "ANNOTATION"}) + */ +#[Attribute(Attribute::TARGET_METHOD)] +class OnlyDebug implements MiddlewareAnnotationInterface +{ +} +``` + +Apart from being a classical annotation/attribute, this class implements the `MiddlewareAnnotationInterface`. This interface is a "marker" interface. It does not have any methods. It is just used to tell GraphQLite that this annotation is to be used by middlewares. + +Now, we can write a middleware that will act upon this annotation. + +```php +namespace App\Middlewares; + +use App\Annotations\OnlyDebug; +use TheCodingMachine\GraphQLite\Middlewares\FieldMiddlewareInterface; +use GraphQL\Type\Definition\FieldDefinition; +use TheCodingMachine\GraphQLite\QueryFieldDescriptor; + +/** + * Middleware in charge of hiding a field if it is annotated with @OnlyDebug and the DEBUG constant is not set + */ +class OnlyDebugFieldMiddleware implements FieldMiddlewareInterface +{ + public function process(QueryFieldDescriptor $queryFieldDescriptor, FieldHandlerInterface $fieldHandler): ?FieldDefinition + { + $annotations = $queryFieldDescriptor->getMiddlewareAnnotations(); + + /** + * @var OnlyDebug $onlyDebug + */ + $onlyDebug = $annotations->getAnnotationByType(OnlyDebug::class); + + if ($onlyDebug !== null && !DEBUG) { + // If the onlyDebug annotation is present, returns null. + // Returning null will hide the field. + return null; + } + + // Otherwise, let's continue the middleware pipe without touching anything. + return $fieldHandler->handle($queryFieldDescriptor); + } +} +``` + +The final thing we have to do is to register the middleware. + +- Assuming you are using the `SchemaFactory` to initialize GraphQLite, you can register the field middleware using: + + ```php + $schemaFactory->addFieldMiddleware(new OnlyDebugFieldMiddleware()); + ``` + +- If you are using the Symfony bundle, you can register your field middleware services by tagging them with the `graphql.field_middleware` tag. diff --git a/website/versioned_docs/version-6.0/file-uploads.mdx b/website/versioned_docs/version-6.0/file-uploads.mdx new file mode 100644 index 0000000000..8c0782f89b --- /dev/null +++ b/website/versioned_docs/version-6.0/file-uploads.mdx @@ -0,0 +1,91 @@ +--- +id: file-uploads +title: File uploads +sidebar_label: File uploads +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +GraphQL does not support natively the notion of file uploads, but an extension to the GraphQL protocol was proposed +to add support for [multipart requests](https://github.com/jaydenseric/graphql-multipart-request-spec). + +## Installation + +GraphQLite supports this extension through the use of the [Ecodev/graphql-upload](https://github.com/Ecodev/graphql-upload) library. + +You must start by installing this package: + +```console +$ composer require ecodev/graphql-upload +``` + +### If you are using the Symfony bundle + +If you are using our Symfony bundle, the file upload middleware is managed by the bundle. You have nothing to do +and can start using it right away. + +### If you are using a PSR-15 compatible framework + +In order to use this, you must first be sure that the `ecodev/graphql-upload` PSR-15 middleware is part of your middleware pipe. + +Simply add `GraphQL\Upload\UploadMiddleware` to your middleware pipe. + +### If you are using another framework not compatible with PSR-15 + +Please check the Ecodev/graphql-upload library [documentation](https://github.com/Ecodev/graphql-upload) +for more information on how to integrate it in your framework. + +## Usage + +To handle an uploaded file, you type-hint against the PSR-7 `UploadedFileInterface`: + + + + +```php +class MyController +{ + #[Mutation] + public function saveDocument(string $name, UploadedFileInterface $file): Document + { + // Some code that saves the document. + $file->moveTo($someDir); + } +} +``` + + + + +```php +class MyController +{ + /** + * @Mutation + */ + public function saveDocument(string $name, UploadedFileInterface $file): Document + { + // Some code that saves the document. + $file->moveTo($someDir); + } +} +``` + + + + +Of course, you need to use a GraphQL client that is compatible with multipart requests. See [jaydenseric/graphql-multipart-request-spec](https://github.com/jaydenseric/graphql-multipart-request-spec#client) for a list of compatible clients. + +The GraphQL client must send the file using the Upload type. + +```graphql +mutation upload($file: Upload!) { + upload(file: $file) +} +``` diff --git a/website/versioned_docs/version-6.0/fine-grained-security.mdx b/website/versioned_docs/version-6.0/fine-grained-security.mdx new file mode 100644 index 0000000000..f5d2fff032 --- /dev/null +++ b/website/versioned_docs/version-6.0/fine-grained-security.mdx @@ -0,0 +1,421 @@ +--- +id: fine-grained-security +title: Fine grained security +sidebar_label: Fine grained security +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +If the [`@Logged` and `@Right` annotations](authentication-authorization.mdx#logged-and-right-annotations) are not +granular enough for your needs, you can use the advanced `@Security` annotation. + +Using the `@Security` annotation, you can write an *expression* that can contain custom logic. For instance: + +- Check that a user can access a given resource +- Check that a user has one right or another right +- ... + +## Using the @Security annotation + +The `@Security` annotation is very flexible: it allows you to pass an expression that can contains custom logic: + + + + +```php +use TheCodingMachine\GraphQLite\Annotations\Security; + +// ... + +#[Query] +#[Security("is_granted('ROLE_ADMIN') or is_granted('POST_SHOW', post)")] +public function getPost(Post $post): array +{ + // ... +} +``` + + + + +```php +use TheCodingMachine\GraphQLite\Annotations\Security; + +// ... + +/** + * @Query + * @Security("is_granted('ROLE_ADMIN') or is_granted('POST_SHOW', post)") + */ +public function getPost(Post $post): array +{ + // ... +} +``` + + + + +The *expression* defined in the `@Security` annotation must conform to [Symfony's Expression Language syntax](https://symfony.com/doc/4.4/components/expression_language/syntax.html) + +
+ If you are a Symfony user, you might already be used to the @Security annotation. Most of the inspiration + of this annotation comes from Symfony. Warning though! GraphQLite's @Security annotation and + Symfony's @Security annotation are slightly different. Especially, the two annotations do not live + in the same namespace! +
+ +## The `is_granted` function + +Use the `is_granted` function to check if a user has a special right. + + + + +```php +#[Security("is_granted('ROLE_ADMIN')")] +``` + + + + +```php +@Security("is_granted('ROLE_ADMIN')") +``` + + + + +is similar to + + + + +```php +#[Right("ROLE_ADMIN")] +``` + + + + +```php +@Right("ROLE_ADMIN") +``` + + + + +In addition, the `is_granted` function accepts a second optional parameter: the "scope" of the right. + + + + +```php +#[Query] +#[Security("is_granted('POST_SHOW', post)")] +public function getPost(Post $post): array +{ + // ... +} +``` + + + + +```php +/** + * @Query + * @Security("is_granted('POST_SHOW', post)") + */ +public function getPost(Post $post): array +{ + // ... +} +``` + + + + +In the example above, the `getPost` method can be called only if the logged user has the 'POST_SHOW' permission on the +`$post` object. You can notice that the `$post` object comes from the parameters. + +## Accessing method parameters + +All parameters passed to the method can be accessed in the `@Security` expression. + + + + +```php +#[Query] +#[Security(expression: "startDate < endDate", statusCode: 400, message: "End date must be after start date")] +public function getPosts(DateTimeImmutable $startDate, DateTimeImmutable $endDate): array +{ + // ... +} +``` + + + + +```php +/** + * @Query + * @Security("startDate < endDate", statusCode=400, message="End date must be after start date") + */ +public function getPosts(DateTimeImmutable $startDate, DateTimeImmutable $endDate): array +{ + // ... +} +``` + + + + + +In the example above, we tweak a bit the Security annotation purpose to do simple input validation. + +## Setting HTTP code and error message + +You can use the `statusCode` and `message` attributes to set the HTTP code and GraphQL error message. + + + + +```php +#[Query] +#[Security(expression: "is_granted('POST_SHOW', post)", statusCode: 404, message: "Post not found (let's pretend the post does not exists!)")] +public function getPost(Post $post): array +{ + // ... +} +``` + + + + +```php +/** + * @Query + * @Security("is_granted('POST_SHOW', post)", statusCode=404, message="Post not found (let's pretend the post does not exists!)") + */ +public function getPost(Post $post): array +{ + // ... +} +``` + + + + +Note: since a single GraphQL call contain many errors, 2 errors might have conflicting HTTP status code. +The resulting status code is up to the GraphQL middleware you use. Most of the time, the status code with the +higher error code will be returned. + +## Setting a default value + +If you do not want an error to be thrown when the security condition is not met, you can use the `failWith` attribute +to set a default value. + + + + +```php +#[Query] +#[Security(expression: "is_granted('CAN_SEE_MARGIN', this)", failWith: null)] +public function getMargin(): float +{ + // ... +} +``` + + + + +```php +/** + * @Field + * @Security("is_granted('CAN_SEE_MARGIN', this)", failWith=null) + */ +public function getMargin(): float +{ + // ... +} +``` + + + + +The `failWith` attribute behaves just like the [`@FailWith` annotation](authentication-authorization.mdx#not-throwing-errors) +but for a given `@Security` annotation. + +You cannot use the `failWith` attribute along `statusCode` or `message` attributes. + +## Accessing the user + +You can use the `user` variable to access the currently logged user. +You can use the `is_logged()` function to check if a user is logged or not. + + + + +```php +#[Query] +#[Security("is_logged() && user.age > 18")] +public function getNSFWImages(): array +{ + // ... +} +``` + + + + +```php +/** + * @Query + * @Security("is_logged() && user.age > 18") + */ +public function getNSFWImages(): array +{ + // ... +} +``` + + + + +## Accessing the current object + +You can use the `this` variable to access any (public) property / method of the current class. + + + + +```php +class Post { + #[Field] + #[Security("this.canAccessBody(user)")] + public function getBody(): array + { + // ... + } + + public function canAccessBody(User $user): bool + { + // Some custom logic here + } +} +``` + + + + +```php +class Post { + /** + * @Field + * @Security("this.canAccessBody(user)") + */ + public function getBody(): array + { + // ... + } + + public function canAccessBody(User $user): bool + { + // Some custom logic here + } +} +``` + + + + +## Available scope + +The `@Security` annotation can be used in any query, mutation or field, so anywhere you have a `@Query`, `@Mutation` +or `@Field` annotation. + +## How to restrict access to a given resource + +The `is_granted` method can be used to restrict access to a specific resource. + + + + +```php +#[Security("is_granted('POST_SHOW', post)")] +``` + + + + +```php +@Security("is_granted('POST_SHOW', post)") +``` + + + + +If you are wondering how to configure these fine-grained permissions, this is not something that GraphQLite handles +itself. Instead, this depends on the framework you are using. + +If you are using Symfony, you will [create a custom voter](https://symfony.com/doc/current/security/voters.html). + +If you are using Laravel, you will [create a Gate or a Policy](https://laravel.com/docs/6.x/authorization). + +If you are using another framework, you need to know that the `is_granted` function simply forwards the call to +the `isAllowed` method of the configured `AuthorizationSerice`. See [Connecting GraphQLite to your framework's security module +](implementing-security.md) for more details diff --git a/website/versioned_docs/version-6.0/getting-started.md b/website/versioned_docs/version-6.0/getting-started.md new file mode 100644 index 0000000000..4eadcfa3fe --- /dev/null +++ b/website/versioned_docs/version-6.0/getting-started.md @@ -0,0 +1,16 @@ +--- +id: getting-started +title: Getting started +sidebar_label: Getting Started +--- + +GraphQLite is a framework agnostic library. You can use it in any PHP project as long as you know how to +inject services in your favorite framework's container. + +Currently, we provide bundle/packages to help you get started with Symfony, Laravel and any framework compatible +with container-interop/service-provider. + +- [Get started with Symfony](symfony-bundle.md) +- [Get started with Laravel](laravel-package.md) +- [Get started with a framework compatible with container-interop/service-provider](universal-service-providers.md) +- [Get started with another framework (or no framework)](other-frameworks.mdx) diff --git a/website/versioned_docs/version-6.0/implementing-security.md b/website/versioned_docs/version-6.0/implementing-security.md new file mode 100644 index 0000000000..ce3c8fb43f --- /dev/null +++ b/website/versioned_docs/version-6.0/implementing-security.md @@ -0,0 +1,57 @@ +--- +id: implementing-security +title: Connecting GraphQLite to your framework's security module +sidebar_label: Connecting security to your framework +--- + +
+ At the time of writing, the Symfony Bundle and the Laravel package handle this implementation. For the latest documentation, please see their respective Github repositories. +
+ +GraphQLite needs to know if a user is logged or not, and what rights it has. +But this is specific of the framework you use. + +To plug GraphQLite to your framework's security mechanism, you will have to provide two classes implementing: + +* `TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface` +* `TheCodingMachine\GraphQLite\Security\AuthorizationServiceInterface` + +Those two interfaces act as adapters between GraphQLite and your framework: + +```php +interface AuthenticationServiceInterface +{ + /** + * Returns true if the "current" user is logged + */ + public function isLogged(): bool; + + /** + * Returns an object representing the current logged user. + * Can return null if the user is not logged. + */ + public function getUser(): ?object; +} +``` + +```php +interface AuthorizationServiceInterface +{ + /** + * Returns true if the "current" user has access to the right "$right" + * + * @param mixed $subject The scope this right applies on. $subject is typically an object or a FQCN. Set $subject to "null" if the right is global. + */ + public function isAllowed(string $right, $subject = null): bool; +} +``` + +You need to write classes that implement these interfaces. Then, you must register those classes with GraphQLite. +It you are [using the `SchemaFactory`](other-frameworks.mdx), you can register your classes using: + +```php +// Configure an authentication service (to resolve the @Logged annotations). +$schemaFactory->setAuthenticationService($myAuthenticationService); +// Configure an authorization service (to resolve the @Right annotations). +$schemaFactory->setAuthorizationService($myAuthorizationService); +``` diff --git a/website/versioned_docs/version-6.0/inheritance-interfaces.mdx b/website/versioned_docs/version-6.0/inheritance-interfaces.mdx new file mode 100644 index 0000000000..05f0404a0e --- /dev/null +++ b/website/versioned_docs/version-6.0/inheritance-interfaces.mdx @@ -0,0 +1,312 @@ +--- +id: inheritance-interfaces +title: Inheritance and interfaces +sidebar_label: Inheritance and interfaces +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Modeling inheritance + +Some of your entities may extend other entities. GraphQLite will do its best to represent this hierarchy of objects in GraphQL using interfaces. + +Let's say you have two classes, `Contact` and `User` (which extends `Contact`): + + + + +```php +#[Type] +class Contact +{ + // ... +} + +#[Type] +class User extends Contact +{ + // ... +} +``` + + + + +```php +/** + * @Type + */ +class Contact +{ + // ... +} + +/** + * @Type + */ +class User extends Contact +{ + // ... +} +``` + + + + +Now, let's assume you have a query that returns a contact: + + + + +```php +class ContactController +{ + #[Query] + public function getContact(): Contact + { + // ... + } +} +``` + + + + +```php +class ContactController +{ + /** + * @Query() + */ + public function getContact(): Contact + { + // ... + } +} +``` + + + + +When writing your GraphQL query, you are able to use fragments to retrieve fields from the `User` type: + +```graphql +contact { + name + ... User { + email + } +} +``` + +Written in [GraphQL type language](https://graphql.org/learn/schema/#type-language), the representation of types +would look like this: + +```graphql +interface ContactInterface { + // List of fields declared in Contact class +} + +type Contact implements ContactInterface { + // List of fields declared in Contact class +} + +type User implements ContactInterface { + // List of fields declared in Contact and User classes +} +``` + +Behind the scene, GraphQLite will detect that the `Contact` class is extended by the `User` class. +Because the class is extended, a GraphQL `ContactInterface` interface is created dynamically. + +The GraphQL `User` type will also automatically implement this `ContactInterface`. The interface contains all the fields +available in the `Contact` type. + +## Mapping interfaces + +If you want to create a pure GraphQL interface, you can also add a `@Type` annotation on a PHP interface. + + + + +```php +#[Type] +interface UserInterface +{ + #[Field] + public function getUserName(): string; +} +``` + + + + +```php +/** + * @Type + */ +interface UserInterface +{ + /** + * @Field + */ + public function getUserName(): string; +} +``` + + + + +This will automatically create a GraphQL interface whose description is: + +```graphql +interface UserInterface { + userName: String! +} +``` + +### Implementing interfaces + +You don't have to do anything special to implement an interface in your GraphQL types. +Simply "implement" the interface in PHP and you are done! + + + + +```php +#[Type] +class User implements UserInterface +{ + public function getUserName(): string; +} +``` + + + + +```php +/** + * @Type + */ +class User implements UserInterface +{ + public function getUserName(): string; +} +``` + + + + +This will translate in GraphQL schema as: + +```graphql +interface UserInterface { + userName: String! +} + +type User implements UserInterface { + userName: String! +} +``` + +Please note that you do not need to put the `@Field` annotation again in the implementing class. + +### Interfaces without an explicit implementing type + +You don't have to explicitly put a `@Type` annotation on the class implementing the interface (though this +is usually a good idea). + + + + +```php +/** + * Look, this class has no #Type attribute + */ +class User implements UserInterface +{ + public function getUserName(): string; +} +``` + +```php +class UserController +{ + #[Query] + public function getUser(): UserInterface // This will work! + { + // ... + } +} +``` + + + + +```php +/** + * Look, this class has no @Type annotation + */ +class User implements UserInterface +{ + public function getUserName(): string; +} +``` + +```php +class UserController +{ + /** + * @Query() + */ + public function getUser(): UserInterface // This will work! + { + // ... + } +} +``` + + + + +
If GraphQLite cannot find a proper GraphQL Object type implementing an interface, it +will create an object type "on the fly".
+ +In the example above, because the `User` class has no `@Type` annotations, GraphQLite will +create a `UserImpl` type that implements `UserInterface`. + +```graphql +interface UserInterface { + userName: String! +} + +type UserImpl implements UserInterface { + userName: String! +} +``` diff --git a/website/versioned_docs/version-6.0/input-types.mdx b/website/versioned_docs/version-6.0/input-types.mdx new file mode 100644 index 0000000000..f2c62afd40 --- /dev/null +++ b/website/versioned_docs/version-6.0/input-types.mdx @@ -0,0 +1,696 @@ +--- +id: input-types +title: Input types +sidebar_label: Input types +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Let's assume you are developing an API that returns a list of cities around a location. + +Your GraphQL query might look like this: + + + + +```php +class MyController +{ + /** + * @return City[] + */ + #[Query] + public function getCities(Location $location, float $radius): array + { + // Some code that returns an array of cities. + } +} + +// Class Location is a simple value-object. +class Location +{ + private $latitude; + private $longitude; + + public function __construct(float $latitude, float $longitude) + { + $this->latitude = $latitude; + $this->longitude = $longitude; + } + + public function getLatitude(): float + { + return $this->latitude; + } + + public function getLongitude(): float + { + return $this->longitude; + } +} +``` + + + + +```php +class MyController +{ + /** + * @Query + * @return City[] + */ + public function getCities(Location $location, float $radius): array + { + // Some code that returns an array of cities. + } +} + +// Class Location is a simple value-object. +class Location +{ + private $latitude; + private $longitude; + + public function __construct(float $latitude, float $longitude) + { + $this->latitude = $latitude; + $this->longitude = $longitude; + } + + public function getLatitude(): float + { + return $this->latitude; + } + + public function getLongitude(): float + { + return $this->longitude; + } +} +``` + + + + +If you try to run this code, you will get the following error: + +``` +CannotMapTypeException: cannot map class "Location" to a known GraphQL input type. Check your TypeMapper configuration. +``` + +You are running into this error because GraphQLite does not know how to handle the `Location` object. + +In GraphQL, an object passed in parameter of a query or mutation (or any field) is called an **Input Type**. + +There are two ways for declaring that type, in GraphQLite: using the [`#[Input]` attribute](input-attribute) or a [Factory method](factory). + +## #\[Input\] Attribute + +Using the `#[Input]` attribute, we can transform the `Location` class, in the example above, into an input type. Just add the `#[Field]` attribute to the corresponding properties: + + + + +```php +#[Input] +class Location +{ + + #[Field] + private ?string $name = null; + + #[Field] + private float $latitude; + + #[Field] + private float $longitude; + + public function __construct(float $latitude, float $longitude) + { + $this->latitude = $latitude; + $this->longitude = $longitude; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getLatitude(): float + { + return $this->latitude; + } + + public function getLongitude(): float + { + return $this->longitude; + } +} +``` + + + + +```php +/** + * @Input + */ +class Location +{ + + /** + * @Field + * @var string|null + */ + private ?string $name = null; + + /** + * @Field + * @var float + */ + private $latitude; + + /** + * @Field + * @var float + */ + private $longitude; + + public function __construct(float $latitude, float $longitude) + { + $this->latitude = $latitude; + $this->longitude = $longitude; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getLatitude(): float + { + return $this->latitude; + } + + public function getLongitude(): float + { + return $this->longitude; + } +} +``` + + + + +Now if you call the `getCities` query, from the controller in the first example, the `Location` object will be automatically instantiated with the user provided, `latitude` / `longitude` properties, and passed to the controller as a parameter. + +There are some important things to notice: + +- The `@Field` annotation is recognized on properties for Input Type, as well as setters. +- There are 3 ways for fields to be resolved: + - Via constructor if corresponding properties are mentioned as parameters with the same names - exactly as in the example above. + - If properties are public, they will be just set without any additional effort - no constructor required. + - For private or protected properties implemented, a public setter is required (if they are not set via the constructor). For example `setLatitude(float $latitude)`. You can also put the `@Field` annotation on the setter, instead of the property, allowing you to have use many other attributes (`Security`, `Right`, `Autowire`, etc.). +- For validation of these Input Types, see the [Custom InputType Validation section](validation#custom-inputtype-validation). +- It's advised to use the `#[Input]` attribute on DTO style input type objects and not directly on your model objects. Using it on your model objects can cause coupling in undesirable ways. + +### Multiple Input Types from the same class + +Simple usage of the `@Input` annotation on a class creates a GraphQL input named by class name + "Input" suffix if a class name does not end with it already. Ex. `LocationInput` for `Location` class. + +You can add multiple `@Input` annotations to the same class, give them different names and link different fields. +Consider the following example: + + + + +```php +#[Input(name: 'CreateUserInput', default: true)] +#[Input(name: 'UpdateUserInput', update: true)] +class UserInput +{ + + #[Field] + public string $username; + + #[Field(for: 'CreateUserInput')] + public string $email; + + #[Field(for: 'CreateUserInput', inputType: 'String!')] + #[Field(for: 'UpdateUserInput', inputType: 'String')] + public string $password; + + protected ?int $age; + + + #[Field] + public function setAge(?int $age): void + { + $this->age = $age; + } +} +``` + + + + +```php +/** + * @Input(name="CreateUserInput", default=true) + * @Input(name="UpdateUserInput", update=true) + */ +class UserInput +{ + + /** + * @Field() + * @var string + */ + public $username; + + /** + * @Field(for="CreateUserInput") + * @var string + */ + public string $email; + + /** + * @Field(for="CreateUserInput", inputType="String!") + * @Field(for="UpdateUserInput", inputType="String") + * @var string|null + */ + public $password; + + /** @var int|null */ + protected $age; + + /** + * @Field() + * @param int|null $age + */ + public function setAge(?int $age): void + { + $this->age = $age; + } +} +``` + + + + +There are 2 input types added to the `UserInput` class: `CreateUserInput` and `UpdateUserInput`. A few notes: +- `CreateUserInput` input will be used by default for this class. +- Field `username` is created for both input types, and it is required because the property type is not nullable. +- Field `email` will appear only for `CreateUserInput` input. +- Field `password` will appear for both. For `CreateUserInput` it'll be the required field and for `UpdateUserInput` optional. +- Field `age` is optional for both input types. + +Note that `update: true` argument for `UpdateUserInput`. It should be used when input type is used for a partial update, +It makes all fields optional and removes all default values from thus prevents setting default values via setters or directly to public properties. +In example above if you use the class as `UpdateUserInput` and set only `username` the other ones will be ignored. +In PHP 7 they will be set to `null`, while in PHP 8 they will be in not initialized state - this can be used as a trick +to check if user actually passed a value for a certain field. + +## Factory + +A **Factory** is a method that takes in parameter all the fields of the input type and return an object. + +Here is an example of factory: + + + + +```php +class MyFactory +{ + /** + * The Factory annotation will create automatically a LocationInput input type in GraphQL. + */ + #[Factory] + public function createLocation(float $latitude, float $longitude): Location + { + return new Location($latitude, $longitude); + } +} +``` + + + + +```php +class MyFactory +{ + /** + * The Factory annotation will create automatically a LocationInput input type in GraphQL. + * + * @Factory() + */ + public function createLocation(float $latitude, float $longitude): Location + { + return new Location($latitude, $longitude); + } +} +``` + + + + +and now, you can run query like this: + +```graphql +query { + getCities(location: { + latitude: 45.0, + longitude: 0.0, + }, + radius: 42) + { + id, + name + } +} +``` + +- Factories must be declared with the **@Factory** annotation. +- The parameters of the factories are the field of the GraphQL input type + +A few important things to notice: + +- 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. + This is usually already the case if you are using a container with auto-wiring capabilities +- We recommend that you put the factories in the same directories as the types. + +### Specifying the input type name + +The GraphQL input type name is derived from the return type of the factory. + +Given the factory below, the return type is "Location", therefore, the GraphQL input type will be named "LocationInput". + + + + +```php +#[Factory] +public function createLocation(float $latitude, float $longitude): Location +{ + return new Location($latitude, $longitude); +} +``` + + + + +```php +/** + * @Factory() + */ +public function createLocation(float $latitude, float $longitude): Location +{ + return new Location($latitude, $longitude); +} +``` + + + + +In case you want to override the input type name, you can use the "name" attribute of the @Factory annotation: + + + + +```php +#[Factory(name: 'MyNewInputName', default: true)] +``` + + + + +```php +/** + * @Factory(name="MyNewInputName", default=true) + */ +``` + + + + +Note that you need to add the "default" attribute is you want your factory to be used by default (more on this in +the next chapter). + +Unless you want to have several factories for the same PHP class, the input type name will be completely transparent +to you, so there is no real reason to customize it. + +### Forcing an input type + +You can use the `@UseInputType` annotation to force an input type of a parameter. + +Let's say you want to force a parameter to be of type "ID", you can use this: + + + + +```php +#[Factory] +#[UseInputType(for: "$id", inputType:"ID!")] +public function getProductById(string $id): Product +{ + return $this->productRepository->findById($id); +} +``` + + + + +```php +/** + * @Factory() + * @UseInputType(for="$id", inputType="ID!") + */ +public function getProductById(string $id): Product +{ + return $this->productRepository->findById($id); +} +``` + + + + +### Declaring several input types for the same PHP class +Available in GraphQLite 4.0+ + +There are situations where a given PHP class might use one factory or another depending on the context. + +This is often the case when your objects map database entities. +In these cases, you can use combine the use of `@UseInputType` and `@Factory` annotation to achieve your goal. + +Here is an annotated sample: + + + + +```php +/** + * This class contains 2 factories to create Product objects. + * The "getProduct" method is used by default to map "Product" classes. + * The "createProduct" method will generate another input type named "CreateProductInput" + */ +class ProductFactory +{ + // ... + + /** + * This factory will be used by default to map "Product" classes. + */ + #[Factory(name: "ProductRefInput", default: true)] + public function getProduct(string $id): Product + { + return $this->productRepository->findById($id); + } + /** + * We specify a name for this input type explicitly. + */ + #[Factory(name: "CreateProductInput", default: false)] + public function createProduct(string $name, string $type): Product + { + return new Product($name, $type); + } +} + +class ProductController +{ + /** + * The "createProduct" factory will be used for this mutation. + */ + #[Mutation] + #[UseInputType(for: "$product", inputType: "CreateProductInput!")] + public function saveProduct(Product $product): Product + { + // ... + } + + /** + * The default "getProduct" factory will be used for this query. + * + * @return Color[] + */ + #[Query] + public function availableColors(Product $product): array + { + // ... + } +} +``` + + + + +```php +/** + * This class contains 2 factories to create Product objects. + * The "getProduct" method is used by default to map "Product" classes. + * The "createProduct" method will generate another input type named "CreateProductInput" + */ +class ProductFactory +{ + // ... + + /** + * This factory will be used by default to map "Product" classes. + * @Factory(name="ProductRefInput", default=true) + */ + public function getProduct(string $id): Product + { + return $this->productRepository->findById($id); + } + /** + * We specify a name for this input type explicitly. + * @Factory(name="CreateProductInput", default=false) + */ + public function createProduct(string $name, string $type): Product + { + return new Product($name, $type); + } +} + +class ProductController +{ + /** + * The "createProduct" factory will be used for this mutation. + * + * @Mutation + * @UseInputType(for="$product", inputType="CreateProductInput!") + */ + public function saveProduct(Product $product): Product + { + // ... + } + + /** + * The default "getProduct" factory will be used for this query. + * + * @Query + * @return Color[] + */ + public function availableColors(Product $product): array + { + // ... + } +} +``` + + + + +### Ignoring some parameters +Available in GraphQLite 4.0+ + +GraphQLite will automatically map all your parameters to an input type. +But sometimes, you might want to avoid exposing some of those parameters. + +Image your `getProductById` has an additional `lazyLoad` parameter. This parameter is interesting when you call +directly the function in PHP because you can have some level of optimisation on your code. But it is not something that +you want to expose in the GraphQL API. Let's hide it! + + + + +```php +#[Factory] +public function getProductById( + string $id, + #[HideParameter] + bool $lazyLoad = true + ): Product +{ + return $this->productRepository->findById($id, $lazyLoad); +} +``` + + + + +```php +/** + * @Factory() + * @HideParameter(for="$lazyLoad") + */ +public function getProductById(string $id, bool $lazyLoad = true): Product +{ + return $this->productRepository->findById($id, $lazyLoad); +} +``` + + + + +With the `@HideParameter` annotation, you can choose to remove from the GraphQL schema any argument. + +To be able to hide an argument, the argument must have a default value. diff --git a/website/versioned_docs/version-6.0/internals.md b/website/versioned_docs/version-6.0/internals.md new file mode 100644 index 0000000000..65624fcc49 --- /dev/null +++ b/website/versioned_docs/version-6.0/internals.md @@ -0,0 +1,142 @@ +--- +id: internals +title: Internals +sidebar_label: Internals +--- + +## Mapping types + +The core of GraphQLite is its ability to map PHP types to GraphQL types. This mapping is performed by a series of +"type mappers". + +GraphQLite contains 4 categories of type mappers: + +- **Parameter mappers** +- **Root type mappers** +- **Recursive (class) type mappers** +- **(class) type mappers** + +```mermaid +graph TD; + classDef custom fill:#cfc,stroke:#7a7,stroke-width:2px,stroke-dasharray: 5, 5; + subgraph RootTypeMapperInterface + NullableTypeMapperAdapter-->CompoundTypeMapper + IteratorTypeMapper-->YourCustomRootTypeMapper + CompoundTypeMapper-->IteratorTypeMapper + YourCustomRootTypeMapper-->MyCLabsEnumTypeMapper + MyCLabsEnumTypeMapper-->EnumTypeMapper + EnumTypeMapper-->BaseTypeMapper + BaseTypeMapper-->FinalRootTypeMapper + end + subgraph RecursiveTypeMapperInterface + BaseTypeMapper-->RecursiveTypeMapper + end + subgraph TypeMapperInterface + RecursiveTypeMapper-->YourCustomTypeMapper + YourCustomTypeMapper-->PorpaginasTypeMapper + PorpaginasTypeMapper-->GlobTypeMapper + end + class YourCustomRootTypeMapper,YourCustomTypeMapper custom; +``` + +## Root type mappers + +(Classes implementing the [`RootTypeMapperInterface`](https://github.com/thecodingmachine/graphqlite/blob/master/src/Mappers/Root/RootTypeMapperInterface.php)) + +These type mappers are the first type mappers called. + +They are responsible for: + +- mapping scalar types (for instance mapping the "int" PHP type to GraphQL Integer type) +- detecting nullable/non-nullable types (for instance interpreting "?int" or "int|null") +- mapping list types (mapping a PHP array to a GraphQL list) +- mapping union types +- mapping enums + +Root type mappers have access to the *context* of a type: they can access the PHP DocBlock and read annotations. +If you want to write a custom type mapper that needs access to annotations, it needs to be a "root type mapper". + +GraphQLite provides 6 classes implementing `RootTypeMapperInterface`: + +- `NullableTypeMapperAdapter`: a type mapper in charge of making GraphQL types non-nullable if the PHP type is non-nullable +- `IteratorTypeMapper`: a type mapper in charge of iterable types (for instance: `MyIterator|User[]`) +- `CompoundTypeMapper`: a type mapper in charge of union types +- `MyCLabsEnumTypeMapper`: maps MyCLabs/enum types to GraphQL enum types (Deprecated: use native enums) +- `EnumTypeMapper`: maps PHP enums to GraphQL enum types +- `BaseTypeMapper`: maps scalar types and lists. Passes the control to the "recursive type mappers" if an object is encountered. +- `FinalRootTypeMapper`: the last type mapper of the chain, used to throw error if no other type mapper managed to handle the type. + +Type mappers are organized in a chain; each type-mapper is responsible for calling the next type mapper. + +```mermaid +graph TD; + classDef custom fill:#cfc,stroke:#7a7,stroke-width:2px,stroke-dasharray: 5, 5; + subgraph RootTypeMapperInterface + NullableTypeMapperAdapter-->CompoundTypeMapper + CompoundTypeMapper-->IteratorTypeMapper + IteratorTypeMapper-->YourCustomRootTypeMapper + YourCustomRootTypeMapper-->MyCLabsEnumTypeMapper + MyCLabsEnumTypeMapper-->EnumTypeMapper + EnumTypeMapper-->BaseTypeMapper + BaseTypeMapper-->FinalRootTypeMapper + end + class YourCustomRootTypeMapper custom; +``` + +## Class type mappers + +(Classes implementing the [`TypeMapperInterface`](https://github.com/thecodingmachine/graphqlite/blob/master/src/Mappers/TypeMapperInterface.php)) + +Class type mappers are mapping PHP classes to GraphQL object types. + +GraphQLite provide 3 default implementations: + +- `CompositeTypeMapper`: a type mapper that delegates mapping to other type mappers using the Composite Design Pattern. +- `GlobTypeMapper`: scans classes in a directory for the `@Type` or `@ExtendType` annotation and maps those to GraphQL types +- `PorpaginasTypeMapper`: maps and class implementing the Porpaginas `Result` interface to a [special paginated type](pagination.mdx). + +### Registering a type mapper in Symfony + +If you are using the GraphQLite Symfony bundle, you can register a type mapper by tagging the service with the "graphql.type_mapper" tag. + +### Registering a type mapper using the SchemaFactory + +If you are using the `SchemaFactory` to bootstrap GraphQLite, you can register a type mapper using the `SchemaFactory::addTypeMapper` method. + +## Recursive type mappers + +(Classes implementing the [`RecursiveTypeMapperInterface`](https://github.com/thecodingmachine/graphqlite/blob/master/src/Mappers/RecursiveTypeMapperInterface.php)) + +There is only one implementation of the `RecursiveTypeMapperInterface`: the `RecursiveTypeMapper`. + +Standard "class type mappers" are mapping a given PHP class to a GraphQL type. But they do not handle class hierarchies. +This is the role of the "recursive type mapper". + +Imagine that class "B" extends class "A" and class "A" maps to GraphQL type "AType". + +Since "B" *is a* "A", the "recursive type mapper" role is to make sure that "B" will also map to GraphQL type "AType". + +## Parameter mapper middlewares + +"Parameter middlewares" are used to decide what argument should be injected into a parameter. + +Let's have a look at a simple query: + +```php +/** + * @Query + * @return Product[] + */ +public function products(ResolveInfo $info): array +``` + +As you may know, [the `ResolveInfo` object injected in this query comes from Webonyx/GraphQL-PHP library](query-plan.mdx). +GraphQLite knows that is must inject a `ResolveInfo` instance because it comes with a [`ResolveInfoParameterHandler`](https://github.com/thecodingmachine/graphqlite/blob/master/src/Mappers/Parameters/ResolveInfoParameterHandler.php) class +that implements the [`ParameterMiddlewareInterface`](https://github.com/thecodingmachine/graphqlite/blob/master/src/Mappers/Parameters/ParameterMiddlewareInterface.php)). + +You can register your own parameter middlewares using the `SchemaFactory::addParameterMiddleware()` method, or by tagging the +service as "graphql.parameter_middleware" if you are using the Symfony bundle. + +
+ Use a parameter middleware if you want to inject an argument in a method and if this argument is not a GraphQL input type or if you want to alter the way input types are imported (for instance if you want to add a validation step) +
diff --git a/website/versioned_docs/version-6.0/laravel-package-advanced.mdx b/website/versioned_docs/version-6.0/laravel-package-advanced.mdx new file mode 100644 index 0000000000..42b0c9640f --- /dev/null +++ b/website/versioned_docs/version-6.0/laravel-package-advanced.mdx @@ -0,0 +1,330 @@ +--- +id: laravel-package-advanced +title: "Laravel package: advanced usage" +sidebar_label: Laravel specific features +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +
+ Be advised! This documentation will be removed in a future release. For current and up-to-date Laravel extension specific documentation, please see the Github repository. +
+ +The Laravel package comes with a number of features to ease the integration of GraphQLite in Laravel. + +## Support for Laravel validation rules + +The GraphQLite Laravel package comes with a special `@Validate` annotation to use Laravel validation rules in your +input types. + + + + +```php +use TheCodingMachine\GraphQLite\Laravel\Annotations\Validate; + +class MyController +{ + #[Mutation] + public function createUser( + #[Validate("email|unique:users")] + string $email, + #[Validate("gte:8")] + string $password + ): User + { + // ... + } +} +``` + + + + +```php +use TheCodingMachine\GraphQLite\Laravel\Annotations\Validate; + +class MyController +{ + /** + * @Mutation + * @Validate(for="$email", rule="email|unique:users") + * @Validate(for="$password", rule="gte:8") + */ + public function createUser(string $email, string $password): User + { + // ... + } +} +``` + + + + +You can use the `@Validate` annotation in any query / mutation / field / factory / decorator. + +If a validation fails to pass, the message will be printed in the "errors" section and you will get a HTTP 400 status code: + +```json +{ + "errors": [ + { + "message": "The email must be a valid email address.", + "extensions": { + "argument": "email", + "category": "Validate" + } + }, + { + "message": "The password must be greater than or equal 8 characters.", + "extensions": { + "argument": "password", + "category": "Validate" + } + } + ] +} +``` + +You can use any validation rule described in [the Laravel documentation](https://laravel.com/docs/6.x/validation#available-validation-rules) + +## Support for pagination + +In your query, if you explicitly return an object that extends the `Illuminate\Pagination\LengthAwarePaginator` class, +the query result will be wrapped in a "paginator" type. + + + + +```php +class MyController +{ + /** + * @return Product[] + */ + #[Query] + public function products(): Illuminate\Pagination\LengthAwarePaginator + { + return Product::paginate(15); + } +} +``` + + + + +```php +class MyController +{ + /** + * @Query + * @return Product[] + */ + public function products(): Illuminate\Pagination\LengthAwarePaginator + { + return Product::paginate(15); + } +} +``` + + + + + +Notice that: + +- the method return type MUST BE `Illuminate\Pagination\LengthAwarePaginator` or a class extending `Illuminate\Pagination\LengthAwarePaginator` +- you MUST add a `@return` statement to help GraphQLite find the type of the list + +Once this is done, you can get plenty of useful information about this page: + +```graphql +products { + items { # The items for the selected page + id + name + } + totalCount # The total count of items. + lastPage # Get the page number of the last available page. + firstItem # Get the "index" of the first item being paginated. + lastItem # Get the "index" of the last item being paginated. + hasMorePages # Determine if there are more items in the data source. + perPage # Get the number of items shown per page. + hasPages # Determine if there are enough items to split into multiple pages. + currentPage # Determine the current page being paginated. + isEmpty # Determine if the list of items is empty or not. + isNotEmpty # Determine if the list of items is not empty. +} +``` + + +
+ Be sure to type hint on the class (Illuminate\Pagination\LengthAwarePaginator) and not on the interface (Illuminate\Contracts\Pagination\LengthAwarePaginator). The interface itself is not iterable (it does not extend Traversable) and therefore, GraphQLite will refuse to iterate over it. +
+ +### Simple paginator + +Note: if you are using `simplePaginate` instead of `paginate`, you can type hint on the `Illuminate\Pagination\Paginator` class. + + + + +```php +class MyController +{ + /** + * @return Product[] + */ + #[Query] + public function products(): Illuminate\Pagination\Paginator + { + return Product::simplePaginate(15); + } +} +``` + + + + +```php +class MyController +{ + /** + * @Query + * @return Product[] + */ + public function products(): Illuminate\Pagination\Paginator + { + return Product::simplePaginate(15); + } +} +``` + + + + +The behaviour will be exactly the same except you will be missing the `totalCount` and `lastPage` fields. + +## Using GraphQLite with Eloquent efficiently + +In GraphQLite, you are supposed to put a `@Field` annotation on each getter. + +Eloquent uses PHP magic properties to expose your database records. +Because Eloquent relies on magic properties, it is quite rare for an Eloquent model to have proper getters and setters. + +So we need to find a workaround. GraphQLite comes with a `@MagicField` annotation to help you +working with magic properties. + + + + +```php +#[Type] +#[MagicField(name: "id", outputType: "ID!")] +#[MagicField(name: "name", phpType: "string")] +#[MagicField(name: "categories", phpType: "Category[]")] +class Product extends Model +{ +} +``` + + + + +```php +/** + * @Type() + * @MagicField(name="id", outputType="ID!") + * @MagicField(name="name", phpType="string") + * @MagicField(name="categories", phpType="Category[]") + */ +class Product extends Model +{ +} +``` + + + + +Please note that since the properties are "magic", they don't have a type. Therefore, +you need to pass either the "outputType" attribute with the GraphQL type matching the property, +or the "phpType" attribute with the PHP type matching the property. + +### Pitfalls to avoid with Eloquent + +When designing relationships in Eloquent, you write a method to expose that relationship this way: + +```php +class User extends Model +{ + /** + * Get the phone record associated with the user. + */ + public function phone() + { + return $this->hasOne('App\Phone'); + } +} +``` + +It would be tempting to put a `@Field` annotation on the `phone()` method, but this will not work. Indeed, +the `phone()` method does not return a `App\Phone` object. It is the `phone` magic property that returns it. + +In short: + +
+ This does not work: + +```php +class User extends Model +{ + /** + * @Field + */ + public function phone() + { + return $this->hasOne('App\Phone'); + } +} +``` + +
+ +
+ This works: + +```php +/** + * @MagicField(name="phone", phpType="App\\Phone") + */ +class User extends Model +{ + public function phone() + { + return $this->hasOne('App\Phone'); + } +} +``` + +
diff --git a/website/versioned_docs/version-6.0/laravel-package.md b/website/versioned_docs/version-6.0/laravel-package.md new file mode 100644 index 0000000000..d7014c2bcc --- /dev/null +++ b/website/versioned_docs/version-6.0/laravel-package.md @@ -0,0 +1,153 @@ +--- +id: laravel-package +title: Getting started with Laravel +sidebar_label: Laravel package +--- + +
+ Be advised! This documentation will be removed in a future release. For current and up-to-date Laravel extension specific documentation, please see the Github repository. +
+ +The GraphQLite-Laravel package is compatible with **Laravel 5.7+**, **Laravel 6.x** and **Laravel 7.x**. + +## Installation + +Open a terminal in your current project directory and run: + +```console +$ composer require thecodingmachine/graphqlite-laravel +``` + +If you want to publish the configuration (in order to edit it), run: + +```console +$ php artisan vendor:publish --provider="TheCodingMachine\GraphQLite\Laravel\Providers\GraphQLiteServiceProvider" +``` + +You can then configure the library by editing `config/graphqlite.php`. + +```php title="config/graphqlite.php" + 'App\\Http\\Controllers', + 'types' => 'App\\', + 'debug' => Debug::RETHROW_UNSAFE_EXCEPTIONS, + 'uri' => env('GRAPHQLITE_URI', '/graphql'), + 'middleware' => ['web'], + 'guard' => ['web'], +]; +``` + +The debug parameters are detailed in the [documentation of the Webonyx GraphQL library](https://webonyx.github.io/graphql-php/error-handling/) +which is used internally by GraphQLite. + +## Configuring CSRF protection + +
By default, the /graphql route is placed under web middleware group which requires a +CSRF token.
+ +You have 3 options: + +- Use the `api` middleware +- Disable CSRF for GraphQL routes +- or configure your GraphQL client to pass the `X-CSRF-TOKEN` with every GraphQL query + +### Use the `api` middleware + +If you plan to use graphql for server-to-server connection only, you should probably configure GraphQLite to use the +`api` middleware instead of the `web` middleware: + +```php title="config/graphqlite.php" + ['api'], + 'guard' => ['api'], +]; +``` + +### Disable CSRF for the /graphql route + +If you plan to use graphql from web browsers and if you want to explicitly allow access from external applications +(through CORS headers), you need to disable the CSRF token. + +Simply add `graphql` to `$except` in `app/Http/Middleware/VerifyCsrfToken.php`. + +### Configuring your GraphQL client + +If you are planning to use `graphql` only from your website domain, then the safest way is to keep CSRF enabled and +configure your GraphQL JS client to pass the CSRF headers on any graphql request. + +The way you do this depends on the Javascript GraphQL client you are using. + +Assuming you are using [Apollo](https://www.apollographql.com/docs/link/links/http/), you need to be sure that Apollo passes the token +back to Laravel on every request. + +```js title="Sample Apollo client setup with CSRF support" +import { ApolloClient, ApolloLink, InMemoryCache, HttpLink } from 'apollo-boost'; + +const httpLink = new HttpLink({ uri: 'https://api.example.com/graphql' }); + +const authLink = new ApolloLink((operation, forward) => { + // Retrieve the authorization token from local storage. + const token = localStorage.getItem('auth_token'); + + // Get the XSRF-TOKEN that is set by Laravel on each request + var cookieValue = document.cookie.replace(/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/, "$1"); + + // Use the setContext method to set the X-CSRF-TOKEN header back. + operation.setContext({ + headers: { + 'X-CSRF-TOKEN': cookieValue + } + }); + + // Call the next link in the middleware chain. + return forward(operation); +}); + +const client = new ApolloClient({ + link: authLink.concat(httpLink), // Chain it with the HttpLink + cache: new InMemoryCache() +}); +``` + +## Adding GraphQL DevTools + +GraphQLite does not include additional GraphQL tooling, such as the GraphiQL editor. +To integrate a web UI to query your GraphQL endpoint with your Laravel installation, +we recommend installing [GraphQL Playground](https://github.com/mll-lab/laravel-graphql-playground) + +```console +$ composer require mll-lab/laravel-graphql-playground +``` + +By default, the playground will be available at `/graphql-playground`. + +Or you can install [Altair GraphQL Client](https://github.com/XKojiMedia/laravel-altair-graphql) + +```console +$ composer require xkojimedia/laravel-altair-graphql +``` + +You can also use any external client with GraphQLite, make sure to point it to the URL defined in the config (`'/graphql'` by default). + +## Troubleshooting HTTP 419 errors + +If HTTP requests to GraphQL endpoint generate responses with the HTTP 419 status code, you have an issue with the configuration of your +CSRF token. Please check again [the paragraph dedicated to CSRF configuration](#configuring-csrf-protection). diff --git a/website/versioned_docs/version-6.0/migrating.md b/website/versioned_docs/version-6.0/migrating.md new file mode 100644 index 0000000000..aa181f7fcc --- /dev/null +++ b/website/versioned_docs/version-6.0/migrating.md @@ -0,0 +1,54 @@ +--- +id: migrating +title: Migrating +sidebar_label: Migrating +--- + +## Migrating from v4.0 to v4.1 + +GraphQLite follows Semantic Versioning. GraphQLite 4.1 is backward compatible with GraphQLite 4.0. See +[semantic versioning](semver.md) for more details. + +There is one exception though: the **ecodev/graphql-upload** package (used to get support for file uploads in GraphQL +input types) is now a "recommended" dependency only. +If you are using GraphQL file uploads, you need to add `ecodev/graphql-upload` to your `composer.json` by running this command: + +```console +$ composer require ecodev/graphql-upload +``` + +## Migrating from v3.0 to v4.0 + +If you are a "regular" GraphQLite user, migration to v4 should be straightforward: + +- Annotations are mostly untouched. The only annotation that is changed is the `@SourceField` annotation. + - Check your code for every places where you use the `@SourceField` annotation: + - The "id" attribute has been remove (`@SourceField(id=true)`). Instead, use `@SourceField(outputType="ID")` + - The "logged", "right" and "failWith" attributes have been removed (`@SourceField(logged=true)`). + Instead, use the annotations attribute with the same annotations you use for the `@Field` annotation: + `@SourceField(annotations={@Logged, @FailWith(null)})` + - If you use magic property and were creating a getter for every magic property (to put a `@Field` annotation on it), + you can now replace this getter with a `@MagicField` annotation. +- In GraphQLite v3, the default was to hide a field from the schema if a user has no access to it. + In GraphQLite v4, the default is to still show this field, but to throw an error if the user makes a query on it + (this way, the schema is the same for all users). If you want the old mode, use the new + [`@HideIfUnauthorized` annotation](annotations-reference.md#hideifunauthorized-annotation) +- If you are using the Symfony bundle, the Laravel package or the Universal module, you must also upgrade those to 4.0. + These package will take care of the wiring for you. Apart for upgrading the packages, you have nothing to do. +- If you are relying on the `SchemaFactory` to bootstrap GraphQLite, you have nothing to do. + +On the other hand, if you are a power user and if you are wiring GraphQLite services yourself (without using the +`SchemaFactory`) or if you implemented custom "TypeMappers", you will need to adapt your code: + +- The `FieldsBuilderFactory` is gone. Directly instantiate `FieldsBuilder` in v4. +- The `CompositeTypeMapper` class has no more constructor arguments. Use the `addTypeMapper` method to register + type mappers in it. +- The `FieldsBuilder` now accept an extra argument: the `RootTypeMapper` that you need to instantiate accordingly. Take + a look at the `SchemaFactory` class for an example of proper configuration. +- The `HydratorInterface` and all implementations are gone. When returning an input object from a TypeMapper, the object + must now implement the `ResolvableMutableInputInterface` (an input object type that contains its own resolver) + +Note: we strongly recommend to use the Symfony bundle, the Laravel package, the Universal module or the SchemaManager +to bootstrap GraphQLite. Wiring directly GraphQLite classes (like the `FieldsBuilder`) into your container is not recommended, +as the signature of the constructor of those classes may vary from one minor release to another. +Use the `SchemaManager` instead. diff --git a/website/versioned_docs/version-6.0/multiple-output-types.mdx b/website/versioned_docs/version-6.0/multiple-output-types.mdx new file mode 100644 index 0000000000..c404c9a5b6 --- /dev/null +++ b/website/versioned_docs/version-6.0/multiple-output-types.mdx @@ -0,0 +1,256 @@ +--- +id: multiple-output-types +title: Mapping multiple output types for the same class +sidebar_label: Class with multiple output types +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Available in GraphQLite 4.0+ + +In most cases, you have one PHP class and you want to map it to one GraphQL output type. + +But in very specific cases, you may want to use different GraphQL output type for the same class. +For instance, depending on the context, you might want to prevent the user from accessing some fields of your object. + +To do so, you need to create 2 output types for the same PHP class. You typically do this using the "default" attribute of the `@Type` annotation. + +## Example + +Here is an example. Say we are manipulating products. When I query a `Product` details, I want to have access to all fields. +But for some reason, I don't want to expose the price field of a product if I query the list of all products. + + + + + + +```php +#[Type] +class Product +{ + // ... + + #[Field] + public function getName(): string + { + return $this->name; + } + + #[Field] + public function getPrice(): ?float + { + return $this->price; + } +} +``` + + + + + + +```php +/** + * @Type() + */ +class Product +{ + // ... + + /** + * @Field() + */ + public function getName(): string + { + return $this->name; + } + + /** + * @Field() + */ + public function getPrice(): ?float + { + return $this->price; + } +} +``` + + + + + +The `Product` class is declaring a classic GraphQL output type named "Product". + + + + +```php +#[Type(class: Product::class, name: "LimitedProduct", default: false)] +#[SourceField(name: "name")] +class LimitedProductType +{ + // ... +} +``` + + + + +```php +/** + * @Type(class=Product::class, name="LimitedProduct", default=false) + * @SourceField(name="name") + */ +class LimitedProductType +{ + // ... +} +``` + + + + + + +The `LimitedProductType` also declares an ["external" type](external-type-declaration.mdx) mapping the `Product` class. +But pay special attention to the `@Type` annotation. + +First of all, we specify `name="LimitedProduct"`. This is useful to avoid having colliding names with the "Product" GraphQL output type +that is already declared. + +Then, we specify `default=false`. This means that by default, the `Product` class should not be mapped to the `LimitedProductType`. +This type will only be used when we explicitly request it. + +Finally, we can write our requests: + + + + +```php +class ProductController +{ + /** + * This field will use the default type. + */ + #[Field] + public function getProduct(int $id): Product { /* ... */ } + + /** + * Because we use the "outputType" attribute, this field will use the other type. + * + * @return Product[] + */ + #[Field(outputType: "[LimitedProduct!]!")] + public function getProducts(): array { /* ... */ } +} +``` + + + + +```php +class ProductController +{ + /** + * This field will use the default type. + * + * @Field + */ + public function getProduct(int $id): Product { /* ... */ } + + /** + * Because we use the "outputType" attribute, this field will use the other type. + * + * @Field(outputType="[LimitedProduct!]!") + * @return Product[] + */ + public function getProducts(): array { /* ... */ } +} +``` + + + + + + +Notice how the "outputType" attribute is used in the `@Field` annotation to force the output type. + +Is a result, when the end user calls the `product` query, we will have the possibility to fetch the `name` and `price` fields, +but if he calls the `products` query, each product in the list will have a `name` field but no `price` field. We managed +to successfully expose a different set of fields based on the query context. + +## Extending a non-default type + +If you want to extend a type using the `@ExtendType` annotation and if this type is declared as non-default, +you need to target the type by name instead of by class. + +So instead of writing: + + + + +```php +#[ExtendType(class: Product::class)] +``` + + + + +```php +/** + * @ExtendType(class=Product::class) + */ +``` + + + + +you will write: + + + + +```php +#[ExtendType(name: "LimitedProduct")] +``` + + + + +```php +/** + * @ExtendType(name="LimitedProduct") + */ +``` + + + + +Notice how we use the "name" attribute instead of the "class" attribute in the `@ExtendType` annotation. diff --git a/website/versioned_docs/version-6.0/mutations.mdx b/website/versioned_docs/version-6.0/mutations.mdx new file mode 100644 index 0000000000..16ab03a72a --- /dev/null +++ b/website/versioned_docs/version-6.0/mutations.mdx @@ -0,0 +1,60 @@ +--- +id: mutations +title: Mutations +sidebar_label: Mutations +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +In GraphQLite, mutations are created [like queries](queries.mdx). + +To create a mutation, you must annotate a method in a controller with the `@Mutation` annotation. + +For instance: + + + + +```php +namespace App\Controller; + +use TheCodingMachine\GraphQLite\Annotations\Mutation; + +class ProductController +{ + #[Mutation] + public function saveProduct(int $id, string $name, ?float $price = null): Product + { + // Some code that saves a product. + } +} +``` + + + + +```php +namespace App\Controller; + +use TheCodingMachine\GraphQLite\Annotations\Mutation; + +class ProductController +{ + /** + * @Mutation + */ + public function saveProduct(int $id, string $name, ?float $price = null): Product + { + // Some code that saves a product. + } +} +``` + + + diff --git a/website/versioned_docs/version-6.0/other-frameworks.mdx b/website/versioned_docs/version-6.0/other-frameworks.mdx new file mode 100644 index 0000000000..392df91151 --- /dev/null +++ b/website/versioned_docs/version-6.0/other-frameworks.mdx @@ -0,0 +1,331 @@ +--- +id: other-frameworks +title: Getting started with any framework +sidebar_label: "Other frameworks / No framework" +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Installation + +Open a terminal in your current project directory and run: + +```console +$ composer require thecodingmachine/graphqlite +``` + +## Requirements + +In order to bootstrap GraphQLite, you will need: + +- A PSR-11 compatible container +- A PSR-16 cache + +Additionally, you will have to route the HTTP requests to the underlying GraphQL library. + +GraphQLite relies on the [webonyx/graphql-php](http://webonyx.github.io/graphql-php/) library internally. +This library plays well with PSR-7 requests and we also provide a [PSR-15 middleware](#psr-15-middleware). + +## Integration + +Webonyx/graphql-php library requires a [Schema](https://webonyx.github.io/graphql-php/type-system/schema/) in order to resolve +GraphQL queries. We provide a `SchemaFactory` class to create such a schema: + +```php +use TheCodingMachine\GraphQLite\SchemaFactory; + +// $cache is a PSR-16 compatible cache +// $container is a PSR-11 compatible container +$factory = new SchemaFactory($cache, $container); +$factory->addControllerNamespace('App\\Controllers\\') + ->addTypeNamespace('App\\'); + +$schema = $factory->createSchema(); +``` + +You can now use this schema with [Webonyx GraphQL facade](https://webonyx.github.io/graphql-php/getting-started/#hello-world) +or the [StandardServer class](https://webonyx.github.io/graphql-php/executing-queries/#using-server). + +The `SchemaFactory` class also comes with a number of methods that you can use to customize your GraphQLite settings. + +```php +// Configure an authentication service (to resolve the @Logged annotations). +$factory->setAuthenticationService(new VoidAuthenticationService()); +// Configure an authorization service (to resolve the @Right annotations). +$factory->setAuthorizationService(new VoidAuthorizationService()); +// Change the naming convention of GraphQL types globally. +$factory->setNamingStrategy(new NamingStrategy()); +// Add a custom type mapper. +$factory->addTypeMapper($typeMapper); +// Add a custom type mapper using a factory to create it. +// Type mapper factories are useful if you need to inject the "recursive type mapper" into your type mapper constructor. +$factory->addTypeMapperFactory($typeMapperFactory); +// Add a root type mapper. +$factory->addRootTypeMapper($rootTypeMapper); +// Add a parameter mapper. +$factory->addParameterMapper($parameterMapper); +// Add a query provider. These are used to find queries and mutations in the application. +$factory->addQueryProvider($queryProvider); +// Add a query provider using a factory to create it. +// Query provider factories are useful if you need to inject the "fields builder" into your query provider constructor. +$factory->addQueryProviderFactory($queryProviderFactory); +// Add custom options to the Webonyx underlying Schema. +$factory->setSchemaConfig($schemaConfig); +// Configures the time-to-live for the GraphQLite cache. Defaults to 2 seconds in dev mode. +$factory->setGlobTtl(2); +// Enables prod-mode (cache settings optimized for best performance). +// This is a shortcut for `$schemaFactory->setGlobTtl(null)` +$factory->prodMode(); +// Enables dev-mode (this is the default mode: cache settings optimized for best developer experience). +// This is a shortcut for `$schemaFactory->setGlobTtl(2)` +$factory->devMode(); +``` + +### GraphQLite context + +Webonyx allows you pass a "context" object when running a query. +For some GraphQLite features to work (namely: the prefetch feature), GraphQLite needs you to initialize the Webonyx context +with an instance of the `TheCodingMachine\GraphQLite\Context\Context` class. + +For instance: + +```php +use TheCodingMachine\GraphQLite\Context\Context; + +$result = GraphQL::executeQuery($schema, $query, null, new Context(), $variableValues); +``` + +## Minimal example + +The smallest working example using no framework is: + +```php +addControllerNamespace('App\\Controllers\\') + ->addTypeNamespace('App\\'); + +$schema = $factory->createSchema(); + +$rawInput = file_get_contents('php://input'); +$input = json_decode($rawInput, true); +$query = $input['query']; +$variableValues = isset($input['variables']) ? $input['variables'] : null; + +$result = GraphQL::executeQuery($schema, $query, null, new Context(), $variableValues); +$output = $result->toArray(); + +header('Content-Type: application/json'); +echo json_encode($output); +``` + +## PSR-15 Middleware + +When using a framework, you will need a way to route your HTTP requests to the `webonyx/graphql-php` library. + +If the framework you are using is compatible with PSR-15 (like Slim PHP or Zend-Expressive / Laminas), GraphQLite +comes with a PSR-15 middleware out of the box. + +In order to get an instance of this middleware, you can use the `Psr15GraphQLMiddlewareBuilder` builder class: + +```php +// $schema is an instance of the GraphQL schema returned by SchemaFactory::createSchema (see previous chapter) +$builder = new Psr15GraphQLMiddlewareBuilder($schema); + +$middleware = $builder->createMiddleware(); + +// You can now inject your middleware in your favorite PSR-15 compatible framework. +// For instance: +$zendMiddlewarePipe->pipe($middleware); +``` + +The builder offers a number of setters to modify its behaviour: + +```php +$builder->setUrl("/graphql"); // Modify the URL endpoint (defaults to /graphql) +$config = $builder->getConfig(); // Returns a Webonyx ServerConfig object. Use this object to configure Webonyx in details. +$builder->setConfig($config); + +$builder->setResponseFactory(new ResponseFactory()); // Set a PSR-18 ResponseFactory (not needed if you are using zend-framework/zend-diactoros ^2 +$builder->setStreamFactory(new StreamFactory()); // Set a PSR-18 StreamFactory (not needed if you are using zend-framework/zend-diactoros ^2 +$builder->setHttpCodeDecider(new HttpCodeDecider()); // Set a class in charge of deciding the HTTP status code based on the response. +``` + +### Example + +In this example, we will focus on getting a working version of GraphQLite using: + +- [Zend Stratigility](https://docs.zendframework.com/zend-stratigility/) as a PSR-15 server +- `mouf/picotainer` (a micro-container) for the PSR-11 container +- `symfony/cache ` for the PSR-16 cache + +The choice of the libraries is really up to you. You can adapt it based on your needs. + +```json title="composer.json" +{ + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "require": { + "thecodingmachine/graphqlite": "^4", + "zendframework/zend-diactoros": "^2", + "zendframework/zend-stratigility": "^3", + "zendframework/zend-httphandlerrunner": "^1.0", + "mouf/picotainer": "^1.1", + "symfony/cache": "^4.2" + }, + "minimum-stability": "dev", + "prefer-stable": true +} +``` + +```php title="index.php" +get(MiddlewarePipe::class), + new SapiStreamEmitter(), + $serverRequestFactory, + $errorResponseGenerator +); +$runner->run(); +``` + +Here we are initializing a Zend `RequestHandler` (it receives requests) and we pass it to a Zend Stratigility `MiddlewarePipe`. +This `MiddlewarePipe` comes from the container declared in the `config/container.php` file: + +```php title="config/container.php" + function(ContainerInterface $container) { + $pipe = new MiddlewarePipe(); + $pipe->pipe($container->get(WebonyxGraphqlMiddleware::class)); + return $pipe; + }, + // The WebonyxGraphqlMiddleware is a PSR-15 compatible + // middleware that exposes Webonyx schemas. + WebonyxGraphqlMiddleware::class => function(ContainerInterface $container) { + $builder = new Psr15GraphQLMiddlewareBuilder($container->get(Schema::class)); + return $builder->createMiddleware(); + }, + CacheInterface::class => function() { + return new ApcuCache(); + }, + Schema::class => function(ContainerInterface $container) { + // The magic happens here. We create a schema using GraphQLite SchemaFactory. + $factory = new SchemaFactory($container->get(CacheInterface::class), $container); + $factory->addControllerNamespace('App\\Controllers\\'); + $factory->addTypeNamespace('App\\'); + return $factory->createSchema(); + } +]); +``` + +Now, we need to add a first query and therefore create a controller. +The application will look into the `App\Controllers` namespace for GraphQLite controllers. + +It assumes that the container has an entry whose name is the controller's fully qualified class name. + + + + + +```php title="src/Controllers/MyController.php" +namespace App\Controllers; + +use TheCodingMachine\GraphQLite\Annotations\Query; + +class MyController +{ + #[Query] + public function hello(string $name): string + { + return 'Hello '.$name; + } +} +``` + + + + +```php title="src/Controllers/MyController.php" +namespace App\Controllers; + +use TheCodingMachine\GraphQLite\Annotations\Query; + +class MyController +{ + /** + * @Query + */ + public function hello(string $name): string + { + return 'Hello '.$name; + } +} +``` + + + + + +```php title="config/container.php" +use App\Controllers\MyController; + +return new Picotainer([ + // ... + + // We declare the controller in the container. + MyController::class => function() { + return new MyController(); + }, +]); +``` + +And we are done! You can now test your query using your favorite GraphQL client. diff --git a/website/versioned_docs/version-6.0/pagination.mdx b/website/versioned_docs/version-6.0/pagination.mdx new file mode 100644 index 0000000000..66b58c863b --- /dev/null +++ b/website/versioned_docs/version-6.0/pagination.mdx @@ -0,0 +1,99 @@ +--- +id: pagination +title: Paginating large result sets +sidebar_label: Pagination +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +It is quite common to have to paginate over large result sets. + +GraphQLite offers a simple way to do that using [Porpaginas](https://github.com/beberlei/porpaginas). + +Porpaginas is a set of PHP interfaces that can be implemented by result iterators. It comes with a native support for +PHP arrays, Doctrine and [TDBM](https://thecodingmachine.github.io/tdbm/doc/limit_offset_resultset.html). + +
If you are a Laravel user, Eloquent does not come with a Porpaginas +iterator. However, the GraphQLite Laravel bundle comes with its own pagination system.
+ +## Installation + +You will need to install the [Porpaginas](https://github.com/beberlei/porpaginas) library to benefit from this feature. + +```bash +$ composer require beberlei/porpaginas +``` + +## Usage + +In your query, simply return a class that implements `Porpaginas\Result`: + + + + +```php +class MyController +{ + /** + * @return Product[] + */ + #[Query] + public function products(): Porpaginas\Result + { + // Some code that returns a list of products + + // If you are using Doctrine, something like: + return new Porpaginas\Doctrine\ORM\ORMQueryResult($doctrineQuery); + } +} +``` + + + + +```php +class MyController +{ + /** + * @Query + * @return Product[] + */ + public function products(): Porpaginas\Result + { + // Some code that returns a list of products + + // If you are using Doctrine, something like: + return new Porpaginas\Doctrine\ORM\ORMQueryResult($doctrineQuery); + } +} +``` + + + + +Notice that: + +- the method return type MUST BE `Porpaginas\Result` or a class implementing `Porpaginas\Result` +- you MUST add a `@return` statement to help GraphQLite find the type of the list + +Once this is done, you can paginate directly from your GraphQL query: + +```graphql +products { + items(limit: 10, offset: 20) { + id + name + } + count +} +``` + +Results are wrapped into an item field. You can use the "limit" and "offset" parameters to apply pagination automatically. + +The "count" field returns the **total count** of items. diff --git a/website/versioned_docs/version-6.0/prefetch-method.mdx b/website/versioned_docs/version-6.0/prefetch-method.mdx new file mode 100644 index 0000000000..9df8877837 --- /dev/null +++ b/website/versioned_docs/version-6.0/prefetch-method.mdx @@ -0,0 +1,201 @@ +--- +id: prefetch-method +title: Prefetching records +sidebar_label: Prefetching records +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## The problem + +GraphQL naive implementations often suffer from the "N+1" problem. + +Consider a request where a user attached to a post must be returned: + +```graphql +{ + posts { + id + user { + id + } + } +} +``` + +A naive implementation will do this: + +- 1 query to fetch the list of posts +- 1 query per post to fetch the user + +Assuming we have "N" posts, we will make "N+1" queries. + +There are several ways to fix this problem. +Assuming you are using a relational database, one solution is to try to look +ahead and perform only one query with a JOIN between "posts" and "users". +This method is described in the ["analyzing the query plan" documentation](query-plan.mdx). + +But this can be difficult to implement. This is also only useful for relational databases. If your data comes from a +NoSQL database or from the cache, this will not help. + +Instead, GraphQLite offers an easier to implement solution: the ability to fetch all fields from a given type at once. + +## The "prefetch" method + + + + +```php +#[Type] +class PostType { + /** + * @param Post $post + * @param mixed $prefetchedUsers + * @return User + */ + #[Field(prefetchMethod: "prefetchUsers")] + public function getUser(Post $post, $prefetchedUsers): User + { + // This method will receive the $prefetchedUsers as second argument. This is the return value of the "prefetchUsers" method below. + // Using this prefetched list, it should be easy to map it to the post + } + + /** + * @param Post[] $posts + * @return mixed + */ + public function prefetchUsers(iterable $posts) + { + // This function is called only once per GraphQL request + // with the list of posts. You can fetch the list of users + // associated with this posts in a single request, + // for instance using a "IN" query in SQL or a multi-fetch + // in your cache back-end. + } +} +``` + + + + +```php +/** + * @Type + */ +class PostType { + /** + * @Field(prefetchMethod="prefetchUsers") + * @param Post $post + * @param mixed $prefetchedUsers + * @return User + */ + public function getUser(Post $post, $prefetchedUsers): User + { + // This method will receive the $prefetchedUsers as second argument. This is the return value of the "prefetchUsers" method below. + // Using this prefetched list, it should be easy to map it to the post + } + + /** + * @param Post[] $posts + * @return mixed + */ + public function prefetchUsers(iterable $posts) + { + // This function is called only once per GraphQL request + // with the list of posts. You can fetch the list of users + // associated with this posts in a single request, + // for instance using a "IN" query in SQL or a multi-fetch + // in your cache back-end. + } +} +``` + + + + + +When the "prefetchMethod" attribute is detected in the "@Field" annotation, the method is called automatically. +The first argument of the method is an array of instances of the main type. +The "prefetchMethod" can return absolutely anything (mixed). The return value will be passed as the second parameter of the "@Field" annotated method. + +## Input arguments + +Field arguments can be set either on the @Field annotated method OR/AND on the prefetchMethod. + +For instance: + + + + +```php +#[Type] +class PostType { + /** + * @param Post $post + * @param mixed $prefetchedComments + * @return Comment[] + */ + #[Field(prefetchMethod: "prefetchComments")] + public function getComments(Post $post, $prefetchedComments): array + { + // ... + } + + /** + * @param Post[] $posts + * @return mixed + */ + public function prefetchComments(iterable $posts, bool $hideSpam, int $filterByScore) + { + // Parameters passed after the first parameter (hideSpam, filterByScore...) are automatically exposed + // as GraphQL arguments for the "comments" field. + } +} +``` + + + + +```php +/** + * @Type + */ +class PostType { + /** + * @Field(prefetchMethod="prefetchComments") + * @param Post $post + * @param mixed $prefetchedComments + * @return Comment[] + */ + public function getComments(Post $post, $prefetchedComments): array + { + // ... + } + + /** + * @param Post[] $posts + * @return mixed + */ + public function prefetchComments(iterable $posts, bool $hideSpam, int $filterByScore) + { + // Parameters passed after the first parameter (hideSpam, filterByScore...) are automatically exposed + // as GraphQL arguments for the "comments" field. + } +} +``` + + + + +The prefetch method MUST be in the same class as the @Field-annotated method and MUST be public. diff --git a/website/versioned_docs/version-6.0/queries.mdx b/website/versioned_docs/version-6.0/queries.mdx new file mode 100644 index 0000000000..34abab54dd --- /dev/null +++ b/website/versioned_docs/version-6.0/queries.mdx @@ -0,0 +1,251 @@ +--- +id: queries +title: Queries +sidebar_label: Queries +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +In GraphQLite, GraphQL queries are created by writing methods in *controller* classes. + +Those classes must be in the controllers namespaces which has been defined when you configured GraphQLite. +For instance, in Symfony, the controllers namespace is `App\Controller` by default. + +## Simple query + +In a controller class, each query method must be annotated with the `@Query` annotation. For instance: + + + + +```php +namespace App\Controller; + +use TheCodingMachine\GraphQLite\Annotations\Query; + +class MyController +{ + #[Query] + public function hello(string $name): string + { + return 'Hello ' . $name; + } +} +``` + + + + +```php +namespace App\Controller; + +use TheCodingMachine\GraphQLite\Annotations\Query; + +class MyController +{ + /** + * @Query + */ + public function hello(string $name): string + { + return 'Hello ' . $name; + } +} +``` + + + + +This query is equivalent to the following [GraphQL type language](https://graphql.org/learn/schema/#type-language): + +```graphql +Type Query { + hello(name: String!): String! +} +``` + +As you can see, GraphQLite will automatically do the mapping between PHP types and GraphQL types. + +
Heads up! If you are not using a framework with an autowiring container (like Symfony or Laravel), please be aware that the MyController class must exist in the container of your application. Furthermore, the identifier of the controller in the container MUST be the fully qualified class name of controller.
+ +## About annotations / attributes + +GraphQLite relies a lot on annotations (we call them attributes since PHP 8). + +It supports both the old "Doctrine annotations" style (`@Query`) and the new PHP 8 attributes (`#[Query]`). + +Read the [Doctrine annotations VS attributes](doctrine-annotations-attributes.mdx) documentation if you are not familiar with this concept. + +## Testing the query + +The default GraphQL endpoint is `/graphql`. + +The easiest way to test a GraphQL endpoint is to use [GraphiQL](https://github.com/graphql/graphiql) or +[Altair](https://altair.sirmuel.design/) clients (they are available as Chrome or Firefox plugins) + +
+ If you are using the Symfony bundle, GraphiQL is also directly embedded.
+ Simply head to http://[path-to-my-app]/graphiql +
+ +Here a query using our simple *Hello World* example: + +![](/img/query1.png) + +## Query with a type + +So far, we simply declared a query. But we did not yet declare a type. + +Let's assume you want to return a product: + + + + +```php +namespace App\Controller; + +use TheCodingMachine\GraphQLite\Annotations\Query; + +class ProductController +{ + #[Query] + public function product(string $id): Product + { + // Some code that looks for a product and returns it. + } +} +``` + + + + +```php +namespace App\Controller; + +use TheCodingMachine\GraphQLite\Annotations\Query; + +class ProductController +{ + /** + * @Query + */ + public function product(string $id): Product + { + // Some code that looks for a product and returns it. + } +} +``` + + + + + + +As the `Product` class is not a scalar type, you must tell GraphQLite how to handle it: + + + + +```php +namespace App\Entities; + +use TheCodingMachine\GraphQLite\Annotations\Field; +use TheCodingMachine\GraphQLite\Annotations\Type; + +#[Type] +class Product +{ + // ... + + #[Field] + public function getName(): string + { + return $this->name; + } + + #[Field] + public function getPrice(): ?float + { + return $this->price; + } +} +``` + + + + +```php +namespace App\Entities; + +use TheCodingMachine\GraphQLite\Annotations\Field; +use TheCodingMachine\GraphQLite\Annotations\Type; + +/** + * @Type() + */ +class Product +{ + // ... + + /** + * @Field() + */ + public function getName(): string + { + return $this->name; + } + + /** + * @Field() + */ + public function getPrice(): ?float + { + return $this->price; + } +} +``` + + + + +The `@Type` annotation is used to inform GraphQLite that the `Product` class is a GraphQL type. + +The `@Field` annotation is used to define the GraphQL fields. This annotation must be put on a **public method**. + +The `Product` class must be in one of the *types* namespaces. As for *controller* classes, you configured this namespace when you installed +GraphQLite. By default, in Symfony, the allowed types namespaces are `App\Entity` and `App\Types`. + +This query is equivalent to the following [GraphQL type language](https://graphql.org/learn/schema/#type-language): + +```graphql +Type Product { + name: String! + price: Float +} +``` + +
+

If you are used to Domain driven design, you probably + realize that the Product class is part of your domain.

+

GraphQL annotations are adding some serialization logic that is out of scope of the domain. + These are just annotations and for most project, this is the fastest and easiest route.

+

If you feel that GraphQL annotations do not belong to the domain, or if you cannot modify the class + directly (maybe because it is part of a third party library), there is another way to create types without annotating + the domain class. We will explore that in the next chapter.

+
diff --git a/website/versioned_docs/version-6.0/query-plan.mdx b/website/versioned_docs/version-6.0/query-plan.mdx new file mode 100644 index 0000000000..98838399d0 --- /dev/null +++ b/website/versioned_docs/version-6.0/query-plan.mdx @@ -0,0 +1,109 @@ +--- +id: query-plan +title: Query plan +sidebar_label: Query plan +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## The problem + +GraphQL naive implementations often suffer from the "N+1" problem. + +Let's have a look at the following query: + +```graphql +{ + products { + name + manufacturer { + name + } + } +} +``` + +A naive implementation will do this: + +- 1 query to fetch the list of products +- 1 query per product to fetch the manufacturer + +Assuming we have "N" products, we will make "N+1" queries. + +There are several ways to fix this problem. Assuming you are using a relational database, one solution is to try to look +ahead and perform only one query with a JOIN between "products" and "manufacturers". + +But how do I know if I should make the JOIN between "products" and "manufacturers" or not? I need to know ahead +of time. + +With GraphQLite, you can answer this question by tapping into the `ResolveInfo` object. + +## Fetching the query plan + +Available in GraphQLite 4.0+ + + + + +```php +use GraphQL\Type\Definition\ResolveInfo; + +class ProductsController +{ + /** + * @return Product[] + */ + #[Query] + public function products(ResolveInfo $info): array + { + if (isset($info->getFieldSelection()['manufacturer']) { + // Let's perform a request with a JOIN on manufacturer + } else { + // Let's perform a request without a JOIN on manufacturer + } + // ... + } +} +``` + + + + +```php +use GraphQL\Type\Definition\ResolveInfo; + +class ProductsController +{ + /** + * @Query + * @return Product[] + */ + public function products(ResolveInfo $info): array + { + if (isset($info->getFieldSelection()['manufacturer']) { + // Let's perform a request with a JOIN on manufacturer + } else { + // Let's perform a request without a JOIN on manufacturer + } + // ... + } +} +``` + + + + + +`ResolveInfo` is a class provided by Webonyx/GraphQL-PHP (the low-level GraphQL library used by GraphQLite). +It contains info about the query and what fields are requested. Using `ResolveInfo::getFieldSelection` you can analyze the query +and decide whether you should perform additional "JOINS" in your query or not. + +
As of the writing of this documentation, the ResolveInfo class is useful but somewhat limited. +The next version of Webonyx/GraphQL-PHP will add a "query plan" +that allows a deeper analysis of the query.
diff --git a/website/versioned_docs/version-6.0/semver.md b/website/versioned_docs/version-6.0/semver.md new file mode 100644 index 0000000000..12931f26a5 --- /dev/null +++ b/website/versioned_docs/version-6.0/semver.md @@ -0,0 +1,45 @@ +--- +id: semver +title: Our backward compatibility promise +sidebar_label: Semantic versioning +--- + +Ensuring smooth upgrades of your project is a priority. That's why we promise you backward compatibility (BC) for all minor GraphQLite releases. You probably recognize this strategy as [Semantic Versioning](https://semver.org/). In short, Semantic Versioning means that only major releases (such as 4.0, 5.0 etc.) are allowed to break backward compatibility. Minor releases (such as 4.0, 4.1 etc.) may introduce new features, but must do so without breaking the existing API of that release branch (4.x in the previous example). + +But sometimes, a new feature is not quite "dry" and we need a bit of time to find the perfect API. +In such cases, we prefer to gather feedback from real-world usage, adapt the API, or remove it altogether. +Doing so is not possible with a no BC-break approach. + +To avoid being bound to our backward compatibility promise, such features can be marked as **unstable** or **experimental** and their classes and methods are marked with the `@unstable` or `@experimental` tag. + +`@unstable` or `@experimental` classes / methods will **not break** in a patch release, but *may be broken* in a minor version. + +As a rule of thumb: + +- If you are a GraphQLite user (using GraphQLite mainly through its annotations), we guarantee strict semantic versioning +- If you are extending GraphQLite features (if you are developing custom annotations, or if you are developing a GraphQlite integration + with a framework...), be sure to check the tags. + +Said otherwise: + +- If you are a GraphQLite user, in your `composer.json`, target a major version: + + ```json + { + "require": { + "thecodingmachine/graphqlite": "^4" + } + } + ``` + +- If you are extending the GraphQLite ecosystem, in your `composer.json`, target a minor version: + + ```json + { + "require": { + "thecodingmachine/graphqlite": "~4.1.0" + } + } + ``` + +Finally, classes / methods annotated with the `@internal` annotation are not meant to be used in your code or third-party library. They are meant for GraphQLite internal usage and they may break anytime. Do not use those directly. diff --git a/website/versioned_docs/version-6.0/symfony-bundle-advanced.mdx b/website/versioned_docs/version-6.0/symfony-bundle-advanced.mdx new file mode 100644 index 0000000000..d96b9fae68 --- /dev/null +++ b/website/versioned_docs/version-6.0/symfony-bundle-advanced.mdx @@ -0,0 +1,229 @@ +--- +id: symfony-bundle-advanced +title: "Symfony bundle: advanced usage" +sidebar_label: Symfony specific features +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +
+ Be advised! This documentation will be removed in a future release. For current and up-to-date Symfony bundle specific documentation, please see the Github repository. +
+ +The Symfony bundle comes with a number of features to ease the integration of GraphQLite in Symfony. + +## Login and logout + +Out of the box, the GraphQLite bundle will expose a "login" and a "logout" mutation as well +as a "me" query (that returns the current user). + +If you need to customize this behaviour, you can edit the "graphqlite.security" configuration key. + +```yaml +graphqlite: + security: + enable_login: auto # Default setting + enable_me: auto # Default setting +``` + +By default, GraphQLite will enable "login" and "logout" mutations and the "me" query if the following conditions are met: + +- the "security" bundle is installed and configured (with a security provider and encoder) +- the "session" support is enabled (via the "framework.session.enabled" key). + +```yaml +graphqlite: + security: + enable_login: on +``` + +By settings `enable_login=on`, you are stating that you explicitly want the login/logout mutations. +If one of the dependencies is missing, an exception is thrown (unlike in default mode where the mutations +are silently discarded). + +```yaml +graphqlite: + security: + enable_login: off +``` + +Use the `enable_login=off` to disable the mutations. + +```yaml +graphqlite: + security: + firewall_name: main # default value +``` + +By default, GraphQLite assumes that your firewall name is "main". This is the default value used in the +Symfony security bundle so it is likely the value you are using. If for some reason you want to use +another firewall, configure the name with `graphqlite.security.firewall_name`. + +## Schema and request security + +You can disable the introspection of your GraphQL API (for instance in production mode) using +the `introspection` configuration properties. + +```yaml +graphqlite: + security: + introspection: false +``` + + +You can set the maximum complexity and depth of your GraphQL queries using the `maximum_query_complexity` +and `maximum_query_depth` configuration properties + +```yaml +graphqlite: + security: + maximum_query_complexity: 314 + maximum_query_depth: 42 +``` + +### Login using the "login" mutation + +The mutation below will log-in a user: + +```graphql +mutation login { + login(userName:"foo", password:"bar") { + userName + roles + } +} +``` + +### Get the current user with the "me" query + +Retrieving the current user is easy with the "me" query: + +```graphql +{ + me { + userName + roles + } +} +``` + +In Symfony, user objects implement `Symfony\Component\Security\Core\User\UserInterface`. +This interface is automatically mapped to a type with 2 fields: + +- `userName: String!` +- `roles: [String!]!` + +If you want to get more fields, just add the `@Type` annotation to your user class: + + + + +```php +#[Type] +class User implements UserInterface +{ + #[Field] + public function getEmail() : string + { + // ... + } + +} +``` + + + + +```php +/** + * @Type + */ +class User implements UserInterface +{ + /** + * @Field + */ + public function getEmail() : string + { + // ... + } + +} +``` + + + + +You can now query this field using an [inline fragment](https://graphql.org/learn/queries/#inline-fragments): + +```graphql +{ + me { + userName + roles + ... on User { + email + } + } +} +``` + +### Logout using the "logout" mutation + +Use the "logout" mutation to log a user out + +```graphql +mutation logout { + logout +} +``` + +## Injecting the Request + +You can inject the Symfony Request object in any query/mutation/field. + +Most of the time, getting the request object is irrelevant. Indeed, it is GraphQLite's job to parse this request and +manage it for you. Sometimes yet, fetching the request can be needed. In those cases, simply type-hint on the request +in any parameter of your query/mutation/field. + + + + +```php +use Symfony\Component\HttpFoundation\Request; + +#[Query] +public function getUser(int $id, Request $request): User +{ + // The $request object contains the Symfony Request. +} +``` + + + + +```php +use Symfony\Component\HttpFoundation\Request; + +/** + * @Query + */ +public function getUser(int $id, Request $request): User +{ + // The $request object contains the Symfony Request. +} +``` + + + diff --git a/website/versioned_docs/version-6.0/symfony-bundle.md b/website/versioned_docs/version-6.0/symfony-bundle.md new file mode 100644 index 0000000000..99822eeddd --- /dev/null +++ b/website/versioned_docs/version-6.0/symfony-bundle.md @@ -0,0 +1,121 @@ +--- +id: symfony-bundle +title: Getting started with Symfony +sidebar_label: Symfony bundle +--- + +
+ Be advised! This documentation will be removed in a future release. For current and up-to-date Symfony bundle specific documentation, please see the Github repository. +
+ +The GraphQLite bundle is compatible with **Symfony 4.x** and **Symfony 5.x**. + +## Applications that use Symfony Flex + +Open a command console, enter your project directory and execute: + +```console +$ composer require thecodingmachine/graphqlite-bundle +``` + +Now, go to the `config/packages/graphqlite.yaml` file and edit the namespaces to match your application. + +```yaml title="config/packages/graphqlite.yaml" +graphqlite: + namespace: + # The namespace(s) that will store your GraphQLite controllers. + # It accept either a string or a list of strings. + controllers: App\GraphQLController\ + # The namespace(s) that will store your GraphQL types and factories. + # It accept either a string or a list of strings. + types: + - App\Types\ + - App\Entity\ +``` + +More advanced parameters are detailed in the ["advanced configuration" section](#advanced-configuration) + +## Applications that don't use Symfony Flex + +Open a terminal in your current project directory and run: + +```console +$ composer require thecodingmachine/graphqlite-bundle +``` + +Enable the library by adding it to the list of registered bundles in the `app/AppKernel.php` file: + + +```php title="app/AppKernel.php" + + Do not put your GraphQL controllers in the App\Controller namespace Symfony applies a particular compiler pass to classes in the App\Controller namespace. This compiler pass will prevent you from using input types. Put your controllers in another namespace. We advise using App\GraphqlController. + + +The Symfony bundle come with a set of advanced features that are not described in this install documentation (like providing a login/logout mutation out of the box). Jump to the ["Symfony specific features"](symfony-bundle-advanced.mdx) documentation of GraphQLite if you want to learn more. diff --git a/website/versioned_docs/version-6.0/troubleshooting.md b/website/versioned_docs/version-6.0/troubleshooting.md new file mode 100644 index 0000000000..862cf9b4e7 --- /dev/null +++ b/website/versioned_docs/version-6.0/troubleshooting.md @@ -0,0 +1,25 @@ +--- +id: troubleshooting +title: Troubleshooting +sidebar_label: Troubleshooting +--- + +**Error: Maximum function nesting level of '100' reached** + +Webonyx's GraphQL library tends to use a very deep stack. +This error does not necessarily mean your code is going into an infinite loop. +Simply try to increase the maximum allowed nesting level in your XDebug conf: + +``` +xdebug.max_nesting_level=500 +``` + + +**Cannot autowire service "_[some input type]_": argument "$..." of method "..." is type-hinted "...", you should configure its value explicitly.** + +The message says that Symfony is trying to instantiate an input type as a service. This can happen if you put your +GraphQLite controllers in the Symfony controller namespace (`App\Controller` by default). Symfony will assume that any +object type-hinted in a method of a controller is a service ([because all controllers are tagged with the "controller.service_arguments" tag](https://symfony.com/doc/current/service_container/3.3-di-changes.html#controllers-are-registered-as-services)) + +To fix this issue, do not put your GraphQLite controller in the same namespace as the Symfony controllers and +reconfigure your `config/graphqlite.yml` file to point to your new namespace. diff --git a/website/versioned_docs/version-6.0/type-mapping.mdx b/website/versioned_docs/version-6.0/type-mapping.mdx new file mode 100644 index 0000000000..c569119196 --- /dev/null +++ b/website/versioned_docs/version-6.0/type-mapping.mdx @@ -0,0 +1,667 @@ +--- +id: type-mapping +title: Type mapping +sidebar_label: Type mapping +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +As explained in the [queries](queries.mdx) section, the job of GraphQLite is to create GraphQL types from PHP types. + +## Scalar mapping + +Scalar PHP types can be type-hinted to the corresponding GraphQL types: + +* `string` +* `int` +* `bool` +* `float` + +For instance: + + + + +```php +namespace App\Controller; + +use TheCodingMachine\GraphQLite\Annotations\Query; + +class MyController +{ + #[Query] + public function hello(string $name): string + { + return 'Hello ' . $name; + } +} +``` + + + + +```php +namespace App\Controller; + +use TheCodingMachine\GraphQLite\Annotations\Query; + +class MyController +{ + /** + * @Query + */ + public function hello(string $name): string + { + return 'Hello ' . $name; + } +} +``` + + + + +## Class mapping + +When returning a PHP class in a query, you must annotate this class using `@Type` and `@Field` annotations: + + + + +```php +namespace App\Entities; + +use TheCodingMachine\GraphQLite\Annotations\Field; +use TheCodingMachine\GraphQLite\Annotations\Type; + +#[Type] +class Product +{ + // ... + + #[Field] + public function getName(): string + { + return $this->name; + } + + #[Field] + public function getPrice(): ?float + { + return $this->price; + } +} +``` + + + + +```php +namespace App\Entities; + +use TheCodingMachine\GraphQLite\Annotations\Field; +use TheCodingMachine\GraphQLite\Annotations\Type; + +/** + * @Type() + */ +class Product +{ + // ... + + /** + * @Field() + */ + public function getName(): string + { + return $this->name; + } + + /** + * @Field() + */ + public function getPrice(): ?float + { + return $this->price; + } +} +``` + + + + +**Note:** The GraphQL output type name generated by GraphQLite is equal to the class name of the PHP class. So if your +PHP class is `App\Entities\Product`, then the GraphQL type will be named "Product". + +In case you have several types with the same class name in different namespaces, you will face a naming collision. +Hopefully, you can force the name of the GraphQL output type using the "name" attribute: + + + + +```php +#[Type(name: "MyProduct")] +class Product { /* ... */ } +``` + + + + +```php +/** + * @Type(name="MyProduct") + */ +class Product { /* ... */ } +``` + + + + + + +## Array mapping + +You can type-hint against arrays (or iterators) as long as you add a detailed `@return` statement in the PHPDoc. + + + + +```php +/** + * @return User[] <=== we specify that the array is an array of User objects. + */ +#[Query] +public function users(int $limit, int $offset): array +{ + // Some code that returns an array of "users". +} +``` + + + + +```php +/** + * @Query + * @return User[] <=== we specify that the array is an array of User objects. + */ +public function users(int $limit, int $offset): array +{ + // Some code that returns an array of "users". +} +``` + + + + +## ID mapping + +GraphQL comes with a native `ID` type. PHP has no such type. + +There are two ways with GraphQLite to handle such type. + +### Force the outputType + + + + +```php +#[Field(outputType: "ID")] +public function getId(): string +{ + // ... +} +``` + + + + +```php +/** + * @Field(outputType="ID") + */ +public function getId(): string +{ + // ... +} +``` + + + + +Using the `outputType` attribute of the `@Field` annotation, you can force the output type to `ID`. + +You can learn more about forcing output types in the [custom types section](custom-types.mdx). + +### ID class + + + + +```php +use TheCodingMachine\GraphQLite\Types\ID; + +#[Field] +public function getId(): ID +{ + // ... +} +``` + + + + +```php +use TheCodingMachine\GraphQLite\Types\ID; + +/** + * @Field + */ +public function getId(): ID +{ + // ... +} +``` + + + + +Note that you can also use the `ID` class as an input type: + + + + +```php +use TheCodingMachine\GraphQLite\Types\ID; + +#[Mutation] +public function save(ID $id, string $name): Product +{ + // ... +} +``` + + + + +```php +use TheCodingMachine\GraphQLite\Types\ID; + +/** + * @Mutation + */ +public function save(ID $id, string $name): Product +{ + // ... +} +``` + + + + +## Date mapping + +Out of the box, GraphQL does not have a `DateTime` type, but we took the liberty to add one, with sensible defaults. + +When used as an output type, `DateTimeImmutable` or `DateTimeInterface` PHP classes are +automatically mapped to this `DateTime` GraphQL type. + + + + +```php +#[Field] +public function getDate(): \DateTimeInterface +{ + return $this->date; +} +``` + + + + +```php +/** + * @Field + */ +public function getDate(): \DateTimeInterface +{ + return $this->date; +} +``` + + + + +The `date` field will be of type `DateTime`. In the returned JSON response to a query, the date is formatted as a string +in the **ISO8601** format (aka ATOM format). + +
+ PHP DateTime type is not supported. +
+ +## Union types + +You can create a GraphQL union type *on the fly* using the pipe `|` operator in the PHPDoc: + + + + +```php +/** + * @return Company|Contact <== can return a company OR a contact. + */ +#[Query] +public function companyOrContact(int $id) +{ + // Some code that returns a company or a contact. +} +``` + + + + +```php +/** + * @Query + * @return Company|Contact <== can return a company OR a contact. + */ +public function companyOrContact(int $id) +{ + // Some code that returns a company or a contact. +} +``` + + + + +## Enum types + +PHP 8.1 introduced native support for Enums. GraphQLite now also supports native enums as of version 5.1. + +```php +#[Type] +enum Status: string +{ + case ON = 'on'; + case OFF = 'off'; + case PENDING = 'pending'; +} +``` + +```php +/** + * @return User[] + */ +#[Query] +public function users(Status $status): array +{ + if ($status === Status::ON) { + // Your logic + } + // ... +} +``` + +```graphql +query users($status: Status!) {} + users(status: $status) { + id + } +} +``` + +By default, the name of the GraphQL enum type will be the name of the class. If you have a naming conflict (two classes +that live in different namespaces with the same class name), you can solve it using the `name` property on the `@Type` annotation: + +```php +namespace Model\User; + +#[Type(name: "UserStatus")] +enum Status: string +{ + // ... +} +``` + + +### Enum types with myclabs/php-enum + +
+ This implementation is now deprecated and will be removed in the future. You are advised to use native enums instead. +
+ +*Prior to version 5.1, GraphQLite only supported Enums through the 3rd party library, [myclabs/php-enum](https://github.com/myclabs/php-enum). If you'd like to use this implementation you'll first need to add this library as a dependency to your application.* + +```bash +$ composer require myclabs/php-enum +``` + +Now, any class extending the `MyCLabs\Enum\Enum` class will be mapped to a GraphQL enum: + + + + +```php +use MyCLabs\Enum\Enum; + +class StatusEnum extends Enum +{ + private const ON = 'on'; + private const OFF = 'off'; + private const PENDING = 'pending'; +} +``` + +```php +/** + * @return User[] + */ +#[Query] +public function users(StatusEnum $status): array +{ + if ($status == StatusEnum::ON()) { + // Note that the "magic" ON() method returns an instance of the StatusEnum class. + // Also, note that we are comparing this instance using "==" (using "===" would fail as we have 2 different instances here) + // ... + } + // ... +} +``` + + + + +```php +use MyCLabs\Enum\Enum; + +class StatusEnum extends Enum +{ + private const ON = 'on'; + private const OFF = 'off'; + private const PENDING = 'pending'; +} +``` + +```php +/** + * @Query + * @return User[] + */ +public function users(StatusEnum $status): array +{ + if ($status == StatusEnum::ON()) { + // Note that the "magic" ON() method returns an instance of the StatusEnum class. + // Also, note that we are comparing this instance using "==" (using "===" would fail as we have 2 different instances here) + // ... + } + // ... +} +``` + + + + +```graphql +query users($status: StatusEnum!) {} + users(status: $status) { + id + } +} +``` + +By default, the name of the GraphQL enum type will be the name of the class. If you have a naming conflict (two classes +that live in different namespaces with the same class name), you can solve it using the `@EnumType` annotation: + + + + +```php +use TheCodingMachine\GraphQLite\Annotations\EnumType; + +#[EnumType(name: "UserStatus")] +class StatusEnum extends Enum +{ + // ... +} +``` + + + + +```php +use TheCodingMachine\GraphQLite\Annotations\EnumType; + +/** + * @EnumType(name="UserStatus") + */ +class StatusEnum extends Enum +{ + // ... +} +``` + + + + +
GraphQLite must be able to find all the classes extending the "MyCLabs\Enum" class +in your project. By default, GraphQLite will look for "Enum" classes in the namespaces declared for the types. For this +reason, your enum classes MUST be in one of the namespaces declared for the types in your GraphQLite +configuration file.
+ +## Deprecation of fields + +You can mark a field as deprecated in your GraphQL Schema by just annotating it with the `@deprecated` PHPDoc annotation. Note that a description (reason) is required for the annotation to be rendered. + +```php +namespace App\Entities; + +use TheCodingMachine\GraphQLite\Annotations\Field; +use TheCodingMachine\GraphQLite\Annotations\Type; + +/** + * @Type() + */ +class Product +{ + // ... + + /** + * @Field() + */ + public function getName(): string + { + return $this->name; + } + + /** + * @Field() + * @deprecated use field `name` instead + */ + public function getProductName(): string + { + return $this->name; + } +} +``` + +This will add the `@deprecated` directive to the field in the GraphQL Schema which sets the `isDeprecated` field to `true` and adds the reason to the `deprecationReason` field in an introspection query. Fields marked as deprecated can still be queried, but will be returned in an introspection query only if `includeDeprecated` is set to `true`. + +```graphql +query { + __type(name: "Product") { + fields(includeDeprecated: true) { + name + isDeprecated + deprecationReason + } + } +} +``` + +## More scalar types + +Available in GraphQLite 4.0+ + +GraphQL supports "custom" scalar types. GraphQLite supports adding more GraphQL scalar types. + +If you need more types, you can check the [GraphQLite Misc. Types library](https://github.com/thecodingmachine/graphqlite-misc-types). +It adds support for more scalar types out of the box in GraphQLite. + +Or if you have some special needs, [you can develop your own scalar types](custom-types#registering-a-custom-scalar-type-advanced). diff --git a/website/versioned_docs/version-6.0/universal-service-providers.md b/website/versioned_docs/version-6.0/universal-service-providers.md new file mode 100644 index 0000000000..8375b0e1e4 --- /dev/null +++ b/website/versioned_docs/version-6.0/universal-service-providers.md @@ -0,0 +1,74 @@ +--- +id: universal-service-providers +title: "Getting started with a framework compatible with container-interop/service-provider" +sidebar_label: Universal service providers +--- + +[container-interop/service-provider](https://github.com/container-interop/service-provider/) is an experimental project +aiming to bring interoperability between framework module systems. + +If your framework is compatible with [container-interop/service-provider](https://github.com/container-interop/service-provider/), +GraphQLite comes with a service provider that you can leverage. + +## Installation + +Open a terminal in your current project directory and run: + +```console +$ composer require thecodingmachine/graphqlite-universal-service-provider +``` + +## Requirements + +In order to bootstrap GraphQLite, you will need: + +- A PSR-16 cache + +Additionally, you will have to route the HTTP requests to the underlying GraphQL library. + +GraphQLite relies on the [webonyx/graphql-php](http://webonyx.github.io/graphql-php/) library internally. +This library plays well with PSR-7 requests and we provide a [PSR-15 middleware](other-frameworks.mdx). + +## Integration + +Webonyx/graphql-php library requires a [Schema](https://webonyx.github.io/graphql-php/type-system/schema/) in order to resolve +GraphQL queries. The service provider provides this `Schema` class. + +[Checkout the the service-provider documentation](https://github.com/thecodingmachine/graphqlite-universal-service-provider) + +## Sample usage + +```json title="composer.json" +{ + "require": { + "mnapoli/simplex": "^0.5", + "thecodingmachine/graphqlite-universal-service-provider": "^3", + "thecodingmachine/symfony-cache-universal-module": "^1" + }, + "minimum-stability": "dev", + "prefer-stable": true +} +``` + +```php title="index.php" +set('graphqlite.namespace.types', ['App\\Types']); +$container->set('graphqlite.namespace.controllers', ['App\\Controllers']); + +$schema = $container->get(Schema::class); + +// or if you want the PSR-15 middleware: + +$middleware = $container->get(Psr15GraphQLMiddlewareBuilder::class); +``` diff --git a/website/versioned_docs/version-6.0/validation.mdx b/website/versioned_docs/version-6.0/validation.mdx new file mode 100644 index 0000000000..ee1a0e93a9 --- /dev/null +++ b/website/versioned_docs/version-6.0/validation.mdx @@ -0,0 +1,287 @@ +--- +id: validation +title: Validation +sidebar_label: User input validation +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +GraphQLite does not handle user input validation by itself. It is out of its scope. + +However, it can integrate with your favorite framework validation mechanism. The way you validate user input will +therefore depend on the framework you are using. + +## Validating user input with Laravel + +If you are using Laravel, jump directly to the [GraphQLite Laravel package advanced documentation](laravel-package-advanced.mdx#support-for-laravel-validation-rules) +to learn how to use the Laravel validation with GraphQLite. + +## Validating user input with Symfony validator + +GraphQLite provides a bridge to use the [Symfony validator](https://symfony.com/doc/current/validation.html) directly in your application. + +- If you are using Symfony and the Symfony GraphQLite bundle, the bridge is available out of the box +- If you are using another framework, the "Symfony validator" component can be used in standalone mode. If you want to + add it to your project, you can require the *thecodingmachine/graphqlite-symfony-validator-bridge* package: + + ```bash + $ composer require thecodingmachine/graphqlite-symfony-validator-bridge + ``` + +### Using the Symfony validator bridge + +Usually, when you use the Symfony validator component, you put annotations in your entities and you validate those entities +using the `Validator` object. + + + + +```php title="UserController.php" +use Symfony\Component\Validator\Validator\ValidatorInterface; +use TheCodingMachine\GraphQLite\Validator\ValidationFailedException + +class UserController +{ + private $validator; + + public function __construct(ValidatorInterface $validator) + { + $this->validator = $validator; + } + + #[Mutation] + public function createUser(string $email, string $password): User + { + $user = new User($email, $password); + + // Let's validate the user + $errors = $this->validator->validate($user); + + // Throw an appropriate GraphQL exception if validation errors are encountered + ValidationFailedException::throwException($errors); + + // No errors? Let's continue and save the user + // ... + } +} +``` + + + + +```php title="UserController.php" +use Symfony\Component\Validator\Validator\ValidatorInterface; +use TheCodingMachine\GraphQLite\Validator\ValidationFailedException + +class UserController +{ + private $validator; + + public function __construct(ValidatorInterface $validator) + { + $this->validator = $validator; + } + + /** + * @Mutation + */ + public function createUser(string $email, string $password): User + { + $user = new User($email, $password); + + // Let's validate the user + $errors = $this->validator->validate($user); + + // Throw an appropriate GraphQL exception if validation errors are encountered + ValidationFailedException::throwException($errors); + + // No errors? Let's continue and save the user + // ... + } +} +``` + + + + +Validation rules are added directly to the object in the domain model: + + + + +```php title="User.php" +use Symfony\Component\Validator\Constraints as Assert; + +class User +{ + #[Assert\Email(message: "The email '{{ value }}' is not a valid email.", checkMX: true)] + private $email; + + /** + * The NotCompromisedPassword assertion asks the "HaveIBeenPawned" service if your password has already leaked or not. + */ + #[Assert\NotCompromisedPassword] + private $password; + + public function __construct(string $email, string $password) + { + $this->email = $email; + $this->password = $password; + } + + // ... +} +``` + + + + +```php title="User.php" +use Symfony\Component\Validator\Constraints as Assert; + +class User +{ + /** + * @Assert\Email( + * message = "The email '{{ value }}' is not a valid email.", + * checkMX = true + * ) + */ + private $email; + + /** + * The NotCompromisedPassword assertion asks the "HaveIBeenPawned" service if your password has already leaked or not. + * @Assert\NotCompromisedPassword + */ + private $password; + + public function __construct(string $email, string $password) + { + $this->email = $email; + $this->password = $password; + } + + // ... +} +``` + + + + +If a validation fails, GraphQLite will return the failed validations in the "errors" section of the JSON response: + +```json +{ + "errors": [ + { + "message": "The email '\"foo@thisdomaindoesnotexistatall.com\"' is not a valid email.", + "extensions": { + "code": "bf447c1c-0266-4e10-9c6c-573df282e413", + "field": "email", + "category": "Validate" + } + } + ] +} +``` + + +### Using the validator directly on a query / mutation / factory ... + +If the data entered by the user is mapped to an object, please use the "validator" instance directly as explained in +the last chapter. It is a best practice to put your validation layer as close as possible to your domain model. + +If the data entered by the user is **not** mapped to an object, you can directly annotate your query, mutation, factory... + +
+ You generally don't want to do this. It is a best practice to put your validation constraints +on your domain objects. Only use this technique if you want to validate user input and user input will not be stored +in a domain object. +
+ +Use the `@Assertion` annotation to validate directly the user input. + +```php +use Symfony\Component\Validator\Constraints as Assert; +use TheCodingMachine\GraphQLite\Validator\Annotations\Assertion; + +/** + * @Query + * @Assertion(for="email", constraint=@Assert\Email()) + */ +public function findByMail(string $email): User +{ + // ... +} +``` + +Notice that the "constraint" parameter contains an annotation (it is an annotation wrapped in an annotation). + +You can also pass an array to the `constraint` parameter: + +```php +@Assertion(for="email", constraint={@Assert\NotBlank(), @Assert\Email()}) +``` + +
Heads up! The "@Assertion" annotation is only available as a Doctrine annotations. You cannot use it as a PHP 8 attributes
+ +## Custom InputType Validation + +GraphQLite also supports a fully custom validation implementation for all input types defined with an `@Input` annotation or PHP8 `#[Input]` attribute. This offers a way to validate input types before they're available as a method parameter of your query and mutation controllers. This way, when you're using your query or mutation controllers, you can feel confident that your input type objects have already been validated. + +
+

It's important to note that this validation implementation does not validate input types created with a factory. If you are creating an input type with a factory, or using primitive parameters in your query/mutation controllers, you should be sure to validate these independently. This is strictly for input type objects.

+ +

You can use one of the framework validation libraries listed above or implement your own validation for these cases. If you're using input type objects for most all of your query and mutation controllers, then there is little additional validation concerns with regards to user input. There are many reasons why you should consider defaulting to an InputType object, as opposed to individual arguments, for your queries and mutations. This is just one additional perk.

+
+ +To get started with validation on input types defined by an `@Input` annotation, you'll first need to register your validator with the `SchemaFactory`. + +```php +$factory = new SchemaFactory($cache, $this->container); +$factory->addControllerNamespace('App\\Controllers'); +$factory->addTypeNamespace('App'); +// Register your validator +$factory->setInputTypeValidator($this->container->get('your_validator')); +$factory->createSchema(); +``` + +Your input type validator must implement the `TheCodingMachine\GraphQLite\Types\InputTypeValidatorInterface`, as shown below: + +```php +interface InputTypeValidatorInterface +{ + /** + * Checks to see if the Validator is currently enabled. + */ + public function isEnabled(): bool; + + /** + * Performs the validation of the InputType. + * + * @param object $input The input type object to validate + */ + public function validate(object $input): void; +} +``` + +The interface is quite simple. Handle all of your own validation logic in the `validate` method. For example, you might use Symfony's annotation based validation in addition to some other custom validation logic. It's really up to you on how you wish to handle your own validation. The `validate` method will receive the input type object populated with the user input. + +You'll notice that the `validate` method has a `void` return. The purpose here is to encourage you to throw an Exception or handle validation output however you best see fit. GraphQLite does it's best to stay out of your way and doesn't make attempts to handle validation output. You can, however, throw an instance of `TheCodingMachine\GraphQLite\Exceptions\GraphQLException` or `TheCodingMachine\GraphQLite\Exceptions\GraphQLAggregateException` as usual (see [Error Handling](error-handling) for more details). + +Also available is the `isEnabled` method. This method is checked before executing validation on an InputType being resolved. You can work out your own logic to selectively enable or disable validation through this method. In most cases, you can simply return `true` to keep it always enabled. + +And that's it, now, anytime an input type is resolved, the validator will be executed on that input type immediately after it has been hydrated with user input. diff --git a/website/versioned_sidebars/version-6.0-sidebars.json b/website/versioned_sidebars/version-6.0-sidebars.json new file mode 100644 index 0000000000..f9b6ef974f --- /dev/null +++ b/website/versioned_sidebars/version-6.0-sidebars.json @@ -0,0 +1,55 @@ +{ + "docs": { + "Introduction": [ + "index" + ], + "Installation": [ + "getting-started", + "symfony-bundle", + "laravel-package", + "universal-service-providers", + "other-frameworks" + ], + "Usage": [ + "queries", + "mutations", + "type-mapping", + "autowiring", + "extend-type", + "external-type-declaration", + "input-types", + "inheritance-interfaces", + "error-handling", + "validation" + ], + "Security": [ + "authentication-authorization", + "fine-grained-security", + "implementing-security" + ], + "Performance": [ + "query-plan", + "prefetch-method" + ], + "Advanced": [ + "file-uploads", + "pagination", + "custom-types", + "field-middlewares", + "argument-resolving", + "extend-input-type", + "multiple-output-types", + "symfony-bundle-advanced", + "laravel-package-advanced", + "internals", + "troubleshooting", + "migrating" + ], + "Reference": [ + "doctrine-annotations-attributes", + "annotations-reference", + "semver", + "changelog" + ] + } +} diff --git a/website/versions.json b/website/versions.json index e5d7291213..06a2358dd8 100644 --- a/website/versions.json +++ b/website/versions.json @@ -1,4 +1,5 @@ [ + "6.0", "5.0", "4.3", "4.2",