diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 228ca70fd1..38803d54df 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -19,6 +19,7 @@ jobs: - "7.2" - "7.3" - "7.4" + - "8.0" dependencies: - "highest" @@ -32,6 +33,7 @@ jobs: with: coverage: "pcov" php-version: "${{ matrix.php-version }}" + tools: composer:v2 - name: "Cache dependencies installed with composer" uses: "actions/cache@v1" @@ -42,16 +44,23 @@ jobs: - name: "Install dependencies with composer" run: "composer install --no-interaction" + if: ${{ matrix.php-version != '8.0' }} + + - name: "Install dependencies with composer. Ignoring platform reqs to bypass a problem with ecodev/graphql-upload available only with latest Webonyx on PHP8." + run: "composer install --no-interaction --ignore-platform-reqs" + if: ${{ matrix.php-version == '8.0' }} - name: "Run tests with phpunit/phpunit" run: "vendor/bin/phpunit" - name: "Run static code analysis with phpstan/phpstan" run: "composer phpstan" - + if: ${{ matrix.php-version == '8.0' }} + - name: "Run coding standard checks with squizlabs/php_codesniffer" run: "composer cs-check" - + if: ${{ matrix.php-version == '8.0' }} + - name: "Archive code coverage results" uses: "actions/upload-artifact@v1" with: diff --git a/composer.json b/composer.json index 92ccd88b49..ef46713e73 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,6 @@ "phpdocumentor/reflection-docblock": "^4.3 || ^5.0", "phpdocumentor/type-resolver": "^1.0.1", "psr/http-message": "^1", - "ecodev/graphql-upload": "^4.0", "webmozart/assert": "^1.4", "symfony/cache": "^4.3 | ^5", "thecodingmachine/cache-utils": "^1", @@ -32,20 +31,22 @@ "psr/http-factory": "^1" }, "require-dev": { - "phpunit/phpunit": "^8.2.4", + "phpunit/phpunit": "^8.2.4||^9.4", "php-coveralls/php-coveralls": "^2.1", "mouf/picotainer": "^1.1", "phpstan/phpstan": "^0.12.25", "beberlei/porpaginas": "^1.2", "myclabs/php-enum": "^1.6.6", - "doctrine/coding-standard": "^7.0", + "doctrine/coding-standard": "^8.2", "phpstan/phpstan-webmozart-assert": "^0.12", "phpstan/extension-installer": "^1.0", "thecodingmachine/phpstan-strict-rules": "^0.12", - "laminas/laminas-diactoros": "^2" + "laminas/laminas-diactoros": "^2", + "ecodev/graphql-upload": "^4.0 || ^5.0 || ^6.0" }, "suggest": { - "beberlei/porpaginas": "If you want automatic pagination in your GraphQL types" + "beberlei/porpaginas": "If you want automatic pagination in your GraphQL types", + "ecodev/graphql-upload": "If you want to support file upload inside GraphQL input types" }, "autoload": { "psr-4": { diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2b4391e0f3..d7b1131893 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,20 @@ title: Changelog sidebar_label: Changelog --- +## 4.1 + +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 this package to your `composer.json`. + +New features: + +- All annotations can now be accessed as PHP 8 attributes + + ## 4.0 This is a complete refactoring from 3.x. While existing annotations are kept compatible, the internals have completely diff --git a/docs/annotations_reference.md b/docs/annotations_reference.md index ecbff87238..8a966e6833 100644 --- a/docs/annotations_reference.md +++ b/docs/annotations_reference.md @@ -4,6 +4,9 @@ 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.md) for more details. + ## @Query annotation The `@Query` annotation is used to declare a GraphQL query. @@ -74,7 +77,7 @@ Attribute | Compulsory | Type | Definition name | *yes* | string | The name of the field. [outputType](custom_types.md) | *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) -annotations | *no* | array | A set of annotations that apply to this field. You would typically used a "@Logged" or "@Right" annotation here. +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. @@ -89,7 +92,7 @@ Attribute | Compulsory | Type | Definition name | *yes* | string | The name of the field. [outputType](custom_types.md) | *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) -annotations | *no* | array | A set of annotations that apply to this field. You would typically used a "@Logged" or "@Right" annotation here. +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. @@ -120,7 +123,7 @@ query / mutation / field (according to the `@Logged` and `@Right` annotations). Attribute | Compulsory | Type | Definition ---------------|------------|------|-------- -*default* | *yes* | mixed | The value to return if the user is not authorized. +value | *yes* | mixed | The value to return if the user is not authorized. ## @HideIfUnauthorized annotation diff --git a/docs/argument_resolving.md b/docs/argument_resolving.md index 846f7a3af9..2d91f175f2 100644 --- a/docs/argument_resolving.md +++ b/docs/argument_resolving.md @@ -15,9 +15,9 @@ 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 /** - * @Query * @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 @@ -58,14 +58,19 @@ If you plan to use annotations while resolving arguments, your annotation should 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 { /** diff --git a/docs/authentication_authorization.md b/docs/authentication_authorization.md index 48a8e716e2..45945a3f3c 100644 --- a/docs/authentication_authorization.md +++ b/docs/authentication_authorization.md @@ -22,7 +22,30 @@ See Connecting GraphQLite to your framework's ## `@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; @@ -44,6 +67,8 @@ class UserController } } ``` + + 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. @@ -62,6 +87,28 @@ If you do not want an error to be thrown when a user attempts to query a field/q 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 { @@ -81,11 +128,37 @@ class UserController } } ``` + ## 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; @@ -105,6 +178,7 @@ class ProductController } } ``` + The `@InjectUser` annotation can be used next to: @@ -123,6 +197,29 @@ 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 { @@ -143,6 +240,7 @@ class UserController } } ``` + 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). diff --git a/docs/autowiring.md b/docs/autowiring.md index 1d08a748f3..9d6be192d8 100644 --- a/docs/autowiring.md +++ b/docs/autowiring.md @@ -17,6 +17,33 @@ the service instance. 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; @@ -43,6 +70,7 @@ class Product } } ``` + When GraphQLite queries the name, it will automatically fetch the translator service. @@ -59,10 +87,8 @@ with a particular service implementation. This makes your code tightly coupled a
Please don't do that: -
    /**
-     * @Field()
-     */
-    public function getName(MyTranslator $translator): string
+
    #[Field]
+    public function getName(#[Autowire] MyTranslator $translator): string
     {
         // Your domain is suddenly tightly coupled to the MyTranslator class.
     }
@@ -74,10 +100,8 @@ Instead, be sure to type-hint against an interface.
 
Do this instead: -
    /**
-     * @Field()
-     */
-    public function getName(TranslatorInterface $translator): string
+
    #[Field]
+    public function getName(#[Autowire] TranslatorInterface $translator): string
     {
         // Good. You can switch translator implementation any time.
     }
@@ -90,11 +114,18 @@ By type-hinting against an interface, your code remains testable and is decouple
 
 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 diff --git a/docs/custom_types.md b/docs/custom_types.md index c6308b51a2..aeaab70fe1 100644 --- a/docs/custom_types.md +++ b/docs/custom_types.md @@ -8,6 +8,20 @@ In some special cases, you want to override the GraphQL return type that is attr For instance: + + +```php +#[Type(class: Product::class)] +class ProductType +{ + #[Field] + public function getId(Product $source): string + { + return $source->getId(); + } +} +``` + ```php /** * @Type(class=Product::class) @@ -15,7 +29,7 @@ For instance: class ProductType { /** - * @Field(name="id") + * @Field */ public function getId(Product $source): string { @@ -23,6 +37,7 @@ class ProductType } } ``` + In the example above, GraphQLite will generate a GraphQL schema with a field `id` of type `string`: @@ -37,11 +52,18 @@ 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 diff --git a/docs/doctrine_annotations_attributes.md b/docs/doctrine_annotations_attributes.md new file mode 100644 index 0000000000..3853956e12 --- /dev/null +++ b/docs/doctrine_annotations_attributes.md @@ -0,0 +1,93 @@ +--- +id: doctrine-annotations-attributes +title: Doctrine annotations VS PHP8 attributes +sidebar_label: Annotations VS Attributes +--- + +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 GraphQLite 5.0
+ +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. + + +
+ +## 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.md): + +**PHP 7+** +``` +/** + * @Field + * @Autowire(for="$productRepository") + */ +public function getProduct(ProductRepository $productRepository) : Product { + //... +} +``` + +**PHP 8** +``` +#[Field] +public function getProduct(#[Autowire] ProductRepository $productRepository) : Product { + //... +} +``` + diff --git a/docs/error_handling.md b/docs/error_handling.md index b368bc194f..3da4d77a85 100644 --- a/docs/error_handling.md +++ b/docs/error_handling.md @@ -139,6 +139,29 @@ 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; @@ -161,6 +184,7 @@ public function createProduct(string $name, float $price): Product } } ``` + ## Webonyx exceptions diff --git a/docs/extend_input_type.md b/docs/extend_input_type.md index 6a3cf82053..81562b6e10 100644 --- a/docs/extend_input_type.md +++ b/docs/extend_input_type.md @@ -23,7 +23,23 @@ or to modify the returned object. 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 { /** @@ -38,11 +54,26 @@ class MyFactory } } ``` + 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 { /** @@ -55,6 +86,7 @@ class MyDecorator } } ``` + In the example above, the "Filter" input type is modified. We add an additional "type" field to the input type. diff --git a/docs/extend_type.md b/docs/extend_type.md index d5813d8668..b0790c596a 100644 --- a/docs/extend_type.md +++ b/docs/extend_type.md @@ -17,6 +17,33 @@ Use the `@ExtendType` annotation to add additional fields to a type that is alre 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; @@ -47,6 +74,7 @@ class Product } } ``` + ```php // You need to use a service to get the name of the product in the correct language. @@ -55,6 +83,33 @@ $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; @@ -83,14 +138,22 @@ class ProductType } } ``` + 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. @@ -119,6 +182,16 @@ If you are using the Symfony bundle (or a framework with autowiring like Laravel 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() @@ -128,6 +201,7 @@ 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. diff --git a/docs/external_type_declaration.md b/docs/external_type_declaration.md index 57ef69bf65..f7ab29a5b1 100644 --- a/docs/external_type_declaration.md +++ b/docs/external_type_declaration.md @@ -16,6 +16,26 @@ For instance: 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; @@ -37,6 +57,7 @@ class ProductType } } ``` + The `ProductType` class must be in the *types* namespace. You configured this namespace when you installed GraphQLite. @@ -53,6 +74,21 @@ In methods with a `@Field` annotation, the first parameter is the *resolved obje 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; @@ -67,6 +103,7 @@ class ProductType { } ``` + By doing so, you let GraphQLite know that the type exposes the `getName` method of the underlying `Product` object. @@ -76,6 +113,24 @@ Internally, GraphQLite will look for methods named `name()`, `getName()` and `is 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; @@ -93,6 +148,7 @@ class ProductType } } ``` + By doing so, you let GraphQLite know that the type exposes "name" and the "price" magic properties of the underlying `Product` object. @@ -123,13 +179,43 @@ class ProductType extends AbstractAnnotatedObjectType } ``` -Any annotations described in the [Authentication and authorization page](authentication_authorization.md) can be used in the `@SourceField` "annotations" attribute. +Any annotations described in the [Authentication and authorization page](authentication_authorization.md), 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; @@ -157,3 +243,4 @@ class ProductType implements FromSourceFieldsInterface } } ``` + diff --git a/docs/features.md b/docs/features.md index fc2a08bcf0..d2b3bf8faf 100644 --- a/docs/features.md +++ b/docs/features.md @@ -21,6 +21,19 @@ A PHP library that allows you to write your GraphQL queries in simple-to-write c 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 { @@ -33,9 +46,25 @@ class ProductController } } ``` + 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() @@ -52,6 +81,7 @@ class Product // ... } ``` + That's it, you're good to go! Query and enjoy! @@ -61,4 +91,4 @@ That's it, you're good to go! Query and enjoy! name } } -``` \ No newline at end of file +``` diff --git a/docs/field_middlewares.md b/docs/field_middlewares.md index 7a75c35c9e..46f9a6a49d 100644 --- a/docs/field_middlewares.md +++ b/docs/field_middlewares.md @@ -73,24 +73,26 @@ It returns the list of annotations applied to your field that implements the `Mi 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. +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. **OnlyDebug.php** ```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, this class implements the `MiddlewareAnnotationInterface`. This interface +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. diff --git a/docs/file_uploads.md b/docs/file_uploads.md index a7624b1ba2..ee919279aa 100644 --- a/docs/file_uploads.md +++ b/docs/file_uploads.md @@ -7,8 +7,16 @@ sidebar_label: File uploads 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 @@ -29,6 +37,20 @@ for more information on how to integrate it in your framework. 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 { @@ -42,6 +64,7 @@ class MyController } } ``` + 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. diff --git a/docs/fine-grained-security.md b/docs/fine-grained-security.md index 6023888fe4..813384c98f 100644 --- a/docs/fine-grained-security.md +++ b/docs/fine-grained-security.md @@ -17,6 +17,21 @@ Using the `@Security` annotation, you can write an *expression* that can contain 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; @@ -31,6 +46,7 @@ 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) @@ -45,18 +61,43 @@ The *expression* defined in the `@Security` annotation must conform to [Symfony' 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 @@ -67,6 +108,7 @@ 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. @@ -75,6 +117,18 @@ In the example above, the `getPost` method can be called only if the logged user All parameters passed to the method can be accessed in the `@Security` expression. + + +**PHP 7** +```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 @@ -85,6 +139,8 @@ public function getPosts(DateTimeImmutable $startDate, DateTimeImmutable $endDat // ... } ``` + + In the example above, we tweak a bit the Security annotation purpose to do simple input validation. @@ -92,6 +148,17 @@ In the example above, we tweak a bit the Security annotation purpose to do simpl 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 @@ -102,6 +169,7 @@ 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 @@ -112,6 +180,17 @@ higher error code will be returned. 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 @@ -122,6 +201,7 @@ public function getMargin(): float // ... } ``` + The `failWith` attribute behaves just like the [`@FailWith` annotation](authentication_authorization.md#not-throwing-errors) but for a given `@Security` annotation. @@ -133,6 +213,17 @@ You cannot use the `failWith` attribute along `statusCode` or `message` attribut 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 @@ -143,11 +234,30 @@ 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 { /** @@ -165,6 +275,7 @@ class Post { } } ``` + ## Available scope @@ -175,9 +286,16 @@ or `@Field` annotation. 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. diff --git a/docs/inheritance-interfaces.md b/docs/inheritance-interfaces.md index acc2092b17..0dbaeb516c 100644 --- a/docs/inheritance-interfaces.md +++ b/docs/inheritance-interfaces.md @@ -10,6 +10,22 @@ Some of your entities may extend other entities. GraphQLite will do its best to Let's say you have two classes, `Contact` and `User` (which extends `Contact`): + + +```php +#[Type] +class Contact +{ + // ... +} + +#[Type] +class User extends Contact +{ + // ... +} +``` + ```php /** * @Type @@ -27,9 +43,23 @@ 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 { @@ -42,6 +72,7 @@ class ContactController } } ``` + When writing your GraphQL query, you are able to use fragments to retrieve fields from the `User` type: @@ -81,6 +112,17 @@ available in the `Contact` type. 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 @@ -93,6 +135,7 @@ interface UserInterface public function getUserName(): string; } ``` + This will automatically create a GraphQL interface whose description is: @@ -107,6 +150,16 @@ interface UserInterface { 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 @@ -116,6 +169,7 @@ class User implements UserInterface public function getUserName(): string; } ``` + This will translate in GraphQL schema as: @@ -136,6 +190,29 @@ Please note that you do not need to put the `@Field` annotation again in the imp 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 @@ -158,6 +235,7 @@ class UserController } } ``` +
If GraphQLite cannot find a proper GraphQL Object type implementing an interface, it will create an object type "on the fly".
diff --git a/docs/input_types.md b/docs/input_types.md index 0ff4177858..41fd64703c 100644 --- a/docs/input_types.md +++ b/docs/input_types.md @@ -8,6 +8,45 @@ Let's admit you are developing an API that returns a list of cities around a loc 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 { @@ -44,6 +83,7 @@ class Location } } ``` + If you try to run this code, you will get the following error: @@ -61,6 +101,22 @@ A **Factory** is a method that takes in parameter all the fields of the input ty Here is an example of factory: + + +``` +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); + } +} +``` + ``` class MyFactory { @@ -75,6 +131,7 @@ class MyFactory } } ``` + and now, you can run query like this: @@ -107,6 +164,8 @@ 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". + + ``` /** * @Factory() @@ -116,6 +175,15 @@ public function createLocation(float $latitude, float $longitude): Location return new Location($latitude, $longitude); } ``` + +``` +#[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: @@ -137,6 +205,17 @@ 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() @@ -147,6 +226,7 @@ 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+ @@ -158,6 +238,61 @@ In these cases, you can use combine the use of `@UseInputType` and `@Factory` an 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. @@ -211,6 +346,7 @@ class ProductController } } ``` + ### Ignoring some parameters Available in GraphQLite 4.0+ @@ -222,6 +358,20 @@ Image your `getProductById` has an additional `lazyLoad` parameter. This paramet 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() @@ -232,6 +382,7 @@ 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. diff --git a/docs/laravel-package-advanced.md b/docs/laravel-package-advanced.md index 4027704321..6283d9417f 100644 --- a/docs/laravel-package-advanced.md +++ b/docs/laravel-package-advanced.md @@ -11,6 +11,26 @@ The Laravel package comes with a number of features to ease the integration of G 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; @@ -27,6 +47,7 @@ class MyController } } ``` + You can use the `@Validate` annotation in any query / mutation / field / factory / decorator. @@ -60,6 +81,22 @@ You can use any validation rule described in [the Laravel documentation](https:/ 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 { @@ -73,6 +110,8 @@ class MyController } } ``` + + Notice that: @@ -110,6 +149,22 @@ iterate over it.
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 { @@ -123,6 +178,7 @@ class MyController } } ``` + The behaviour will be exactly the same except you will be missing the `totalCount` and `lastPage` fields. @@ -136,17 +192,30 @@ Because Eloquent relies on magic properties, it is quite rare for an Eloquent mo 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[]") + * @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, diff --git a/docs/multiple_output_types.md b/docs/multiple_output_types.md index 7fbd69c207..cc12012321 100644 --- a/docs/multiple_output_types.md +++ b/docs/multiple_output_types.md @@ -15,7 +15,33 @@ To do so, you need to create 2 output types for the same PHP class. You typicall ## 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. +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 /** @@ -43,8 +69,21 @@ class Product } ``` + + 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) @@ -53,19 +92,13 @@ The `Product` class is declaring a classic GraphQL output type named "Product". class LimitedProductType { // ... - - /** - * @Field() - */ - public function getName(Product $product): string - { - return $product->getName(); - } } ``` + -The `LimitedProductType` also declares a ["external" type](external_type_declaration.md) mapping the `Product` class. + +The `LimitedProductType` also declares an ["external" type](external_type_declaration.md) 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 @@ -76,6 +109,27 @@ 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 { @@ -94,7 +148,10 @@ class ProductController */ public function getProducts(): array { /* ... */ } } -``` +``` + + + Notice how the "outputType" attribute is used in the `@Field` annotation to force the output type. @@ -109,18 +166,32 @@ 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/docs/mutations.md b/docs/mutations.md index ef27eaf6f6..e57095aecf 100644 --- a/docs/mutations.md +++ b/docs/mutations.md @@ -10,6 +10,23 @@ To create a mutation, you must annotate a method in a controller with the `@Muta 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; @@ -26,3 +43,4 @@ class ProductController } } ``` + diff --git a/docs/other_frameworks.md b/docs/other_frameworks.md index 06de4d22d6..fe6b50d797 100644 --- a/docs/other_frameworks.md +++ b/docs/other_frameworks.md @@ -275,6 +275,23 @@ It assumes that the container has an entry whose name is the controller's fully **src/Controllers/MyController.php** + + +```php +namespace App\Controllers; + +use TheCodingMachine\GraphQLite\Annotations\Query; + +class MyController +{ + #[Query] + public function hello(string $name): string + { + return 'Hello '.$name; + } +} +``` + ```php namespace App\Controllers; @@ -291,6 +308,8 @@ class MyController } } ``` + + **config/container.php** diff --git a/docs/pagination.md b/docs/pagination.md index a6dee248f4..c8fdf2be47 100644 --- a/docs/pagination.md +++ b/docs/pagination.md @@ -26,6 +26,25 @@ $ composer require beberlei/porpaginas 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 { @@ -42,6 +61,7 @@ class MyController } } ``` + Notice that: diff --git a/docs/prefetch_method.md b/docs/prefetch_method.md index 20723bae48..c19a83136c 100644 --- a/docs/prefetch_method.md +++ b/docs/prefetch_method.md @@ -40,6 +40,38 @@ Instead, GraphQLite offers an easier to implement solution: the ability to fetch ## 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 @@ -71,6 +103,8 @@ class PostType { } } ``` + + 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. @@ -82,6 +116,34 @@ Field arguments can be set either on the @Field annotated method OR/AND on the p 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 @@ -109,5 +171,6 @@ class PostType { } } ``` + The prefetch method MUST be in the same class as the @Field-annotated method and MUST be public. diff --git a/docs/queries.md b/docs/queries.md index 1b5f53bce1..1dced36b99 100644 --- a/docs/queries.md +++ b/docs/queries.md @@ -13,6 +13,26 @@ For instance, in Symfony, the controllers namespace is `App\Controller` by defau 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; @@ -30,6 +50,8 @@ class MyController } ``` + + This query is equivalent to the following [GraphQL type language](https://graphql.org/learn/schema/#type-language): ```graphql @@ -42,6 +64,14 @@ As you can see, GraphQLite will automatically do the mapping between PHP types a
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.md) documentation if you are not familiar with this concept. + ## Testing the query The default GraphQL endpoint is `/graphql`. @@ -64,6 +94,23 @@ 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; @@ -80,9 +127,39 @@ class ProductController } } ``` + + + 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; @@ -113,6 +190,7 @@ class Product } } ``` + The `@Type` annotation is used to inform GraphQLite that the `Product` class is a GraphQL type. diff --git a/docs/query_plan.md b/docs/query_plan.md index b24a13b35c..c98c9f9a00 100644 --- a/docs/query_plan.md +++ b/docs/query_plan.md @@ -40,6 +40,29 @@ With GraphQLite, you can answer this question by tapping into the `ResolveInfo` 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; @@ -60,6 +83,8 @@ class ProductsController } } ``` + + `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 diff --git a/docs/symfony-bundle-advanced.md b/docs/symfony-bundle-advanced.md index bd73d9463c..2f279ac6de 100644 --- a/docs/symfony-bundle-advanced.md +++ b/docs/symfony-bundle-advanced.md @@ -109,6 +109,21 @@ This interface is automatically mapped to a type with 2 fields: 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 @@ -125,6 +140,7 @@ class User implements UserInterface } ``` + You can now query this field using an [inline fragment](https://graphql.org/learn/queries/#inline-fragments): @@ -158,6 +174,18 @@ Most of the time, getting the request object is irrelevant. Indeed, it is GraphQ 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; @@ -169,3 +197,4 @@ public function getUser(int $id, Request $request): User // The $request object contains the Symfony Request. } ``` + diff --git a/docs/type_mapping.md b/docs/type_mapping.md index 9e73c37228..f189cfb60b 100644 --- a/docs/type_mapping.md +++ b/docs/type_mapping.md @@ -17,6 +17,23 @@ Scalar PHP types can be type-hinted to the corresponding GraphQL types: 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; @@ -33,11 +50,39 @@ class MyController } } ``` + ## 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; @@ -68,6 +113,7 @@ class Product } } ``` + **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". @@ -75,12 +121,20 @@ PHP class is `App\Entities\Product`, then the GraphQL type will be named "Produc 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 { /* ... */ } ``` + @@ -89,6 +143,19 @@ to map your code to a GraphQL interface.
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 @@ -99,6 +166,7 @@ public function users(int $limit, int $offset): array // Some code that returns an array of "users". } ``` + ## ID mapping @@ -108,6 +176,16 @@ 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") @@ -117,6 +195,7 @@ public function getId(): string // ... } ``` + Using the `outputType` attribute of the `@Field` annotation, you can force the output type to `ID`. @@ -124,6 +203,18 @@ You can learn more about forcing output types in the [custom types section](cust ### ID class + + +```php +use TheCodingMachine\GraphQLite\Types\ID; + +#[Field] +public function getId(): ID +{ + // ... +} +``` + ```php use TheCodingMachine\GraphQLite\Types\ID; @@ -135,9 +226,22 @@ 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; @@ -149,6 +253,7 @@ public function save(ID $id, string $name): Product // ... } ``` + ## Date mapping @@ -157,6 +262,16 @@ Out of the box, GraphQL does not have a `DateTime` type, but we took the liberty 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 @@ -166,6 +281,7 @@ 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). @@ -178,6 +294,19 @@ in the **ISO8601** format (aka ATOM format). 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 @@ -188,6 +317,7 @@ public function companyOrContact(int $id) // Some code that returns a company or a contact. } ``` + ## Enum types @@ -205,6 +335,35 @@ $ 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; @@ -231,6 +390,7 @@ public function users(StatusEnum $status): array // ... } ``` + ```graphql query users($status: StatusEnum!) {} @@ -243,6 +403,18 @@ query users($status: StatusEnum!) {} 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; @@ -254,6 +426,7 @@ 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 diff --git a/docs/validation.md b/docs/validation.md index e8dbd4ed20..e619b4981c 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -30,6 +30,39 @@ GraphQLite provides a bridge to use the [Symfony validator](https://symfony.com/ Usually, when you use the Symfony validator component, you put annotations in your entities and you validate those entities using the `Validator` object. + + +**UserController.php** +```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 + // ... + } +} +``` + **UserController.php** ```php use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -62,9 +95,37 @@ class UserController } } ``` + Validation rules are added directly to the object in the domain model: + + +**User.php** +```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; + } + + // ... +} +``` + **User.php** ```php use Symfony\Component\Validator\Constraints as Assert; @@ -94,6 +155,7 @@ class User // ... } ``` + If a validation fails, GraphQLite will return the failed validations in the "errors" section of the JSON response: @@ -147,3 +209,5 @@ 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
diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 48120f3e99..3f5d8700bd 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -18,17 +18,13 @@ src/Types/TypeAnnotatedInterfaceType.php src/Mappers/Proxys/* - - - src/Middlewares/ServiceResolver.php - src/QueryFieldDescriptor.php - + diff --git a/phpstan.neon b/phpstan.neon index 0f24b48c58..9e9c137ed5 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -31,7 +31,7 @@ parameters: message: '#Unreachable statement - code above always terminates.#' path: src/Http/WebonyxGraphqlMiddleware.php - - message: '#Property TheCodingMachine\GraphQLite\Annotations\Type::\$class \(class-string\\|null\) does not accept string.#' + message: '#Property TheCodingMachine\\GraphQLite\\Annotations\\Type::\$class \(class-string\\|null\) does not accept string.#' path: src/Annotations/Type.php - message: '#Method TheCodingMachine\\GraphQLite\\AnnotationReader::getMethodAnnotations\(\) should return array but returns array.#' diff --git a/src/AggregateControllerQueryProvider.php b/src/AggregateControllerQueryProvider.php index 5c6eb5bde9..929a0b78e8 100644 --- a/src/AggregateControllerQueryProvider.php +++ b/src/AggregateControllerQueryProvider.php @@ -7,6 +7,7 @@ use GraphQL\Type\Definition\FieldDefinition; use Psr\Container\ContainerInterface; use TheCodingMachine\GraphQLite\Mappers\DuplicateMappingException; + use function array_filter; use function array_intersect_key; use function array_keys; diff --git a/src/AnnotationReader.php b/src/AnnotationReader.php index cffda2a7fb..efd8976fd7 100644 --- a/src/AnnotationReader.php +++ b/src/AnnotationReader.php @@ -26,6 +26,7 @@ use TheCodingMachine\GraphQLite\Annotations\SourceFieldInterface; use TheCodingMachine\GraphQLite\Annotations\Type; use Webmozart\Assert\Assert; + use function array_diff_key; use function array_filter; use function array_key_exists; @@ -35,11 +36,14 @@ use function assert; use function get_class; use function in_array; +use function is_a; use function reset; use function strpos; use function strrpos; use function substr; +use const PHP_MAJOR_VERSION; + class AnnotationReader { /** @var Reader */ @@ -113,9 +117,12 @@ public function getExtendTypeAnnotation(ReflectionClass $refClass): ?ExtendType return $extendType; } - public function getRequestAnnotation(ReflectionMethod $refMethod, string $annotationName): ?AbstractRequest + /** + * @param class-string $annotationClass + */ + public function getRequestAnnotation(ReflectionMethod $refMethod, string $annotationClass): ?AbstractRequest { - $queryAnnotation = $this->getMethodAnnotation($refMethod, $annotationName); + $queryAnnotation = $this->getMethodAnnotation($refMethod, $annotationClass); assert($queryAnnotation instanceof AbstractRequest || $queryAnnotation === null); return $queryAnnotation; @@ -214,6 +221,21 @@ public function getParameterAnnotationsPerParameter(array $refParameters): array } } + // Now, let's add PHP 8 parameter attributes + if (PHP_MAJOR_VERSION >= 8) { + foreach ($refParameters as $refParameter) { + $attributes = $refParameter->getAttributes(); + $parameterAnnotationsPerParameter[$refParameter->getName()] = array_merge($parameterAnnotationsPerParameter[$refParameter->getName()] ?? [], array_map( + static function ($attribute) { + return $attribute->newInstance(); + }, + array_filter($attributes, static function ($annotation): bool { + return is_a($annotation->getName(), ParameterAnnotationInterface::class, true); + }) + )); + } + } + return array_map(static function (array $parameterAnnotations) { return new ParameterAnnotations($parameterAnnotations); }, $parameterAnnotationsPerParameter); @@ -243,12 +265,23 @@ private function getClassAnnotation(ReflectionClass $refClass, string $annotatio { $type = null; try { + // If attribute & annotation, let's prefer the PHP 8 attribute + if (PHP_MAJOR_VERSION >= 8) { + $attribute = $refClass->getAttributes($annotationClass)[0] ?? null; + if ($attribute) { + $instance = $attribute->newInstance(); + assert($instance instanceof $annotationClass); + return $instance; + } + } + $type = $this->reader->getClassAnnotation($refClass, $annotationClass); assert($type === null || $type instanceof $annotationClass); } catch (AnnotationException $e) { switch ($this->mode) { case self::STRICT_MODE: throw $e; + case self::LAX_MODE: if ($this->isErrorImportant($annotationClass, $refClass->getDocComment() ?: '', $refClass->getName())) { throw $e; @@ -268,6 +301,8 @@ private function getClassAnnotation(ReflectionClass $refClass, string $annotatio /** * Returns a method annotation and handles correctly errors. + * + * @param class-string $annotationClass */ private function getMethodAnnotation(ReflectionMethod $refMethod, string $annotationClass): ?object { @@ -277,11 +312,20 @@ private function getMethodAnnotation(ReflectionMethod $refMethod, string $annota } try { + // If attribute & annotation, let's prefer the PHP 8 attribute + if (PHP_MAJOR_VERSION >= 8) { + $attribute = $refMethod->getAttributes($annotationClass)[0] ?? null; + if ($attribute) { + return $this->methodAnnotationCache[$cacheKey] = $attribute->newInstance(); + } + } + return $this->methodAnnotationCache[$cacheKey] = $this->reader->getMethodAnnotation($refMethod, $annotationClass); } catch (AnnotationException $e) { switch ($this->mode) { case self::STRICT_MODE: throw $e; + case self::LAX_MODE: if ($this->isErrorImportant($annotationClass, $refMethod->getDocComment() ?: '', $refMethod->getDeclaringClass()->getName())) { throw $e; @@ -331,6 +375,17 @@ public function getClassAnnotations(ReflectionClass $refClass, string $annotatio $toAddAnnotations[] = array_filter($allAnnotations, static function ($annotation) use ($annotationClass): bool { return $annotation instanceof $annotationClass; }); + if (PHP_MAJOR_VERSION >= 8) { + $attributes = $refClass->getAttributes(); + $toAddAnnotations[] = array_map( + static function ($attribute) { + return $attribute->newInstance(); + }, + array_filter($attributes, static function ($annotation) use ($annotationClass): bool { + return is_a($annotation->getName(), $annotationClass, true); + }) + ); + } } catch (AnnotationException $e) { if ($this->mode === self::STRICT_MODE) { throw $e; @@ -379,6 +434,17 @@ public function getMethodAnnotations(ReflectionMethod $refMethod, string $annota $toAddAnnotations = array_filter($allAnnotations, static function ($annotation) use ($annotationClass): bool { return $annotation instanceof $annotationClass; }); + if (PHP_MAJOR_VERSION >= 8) { + $attributes = $refMethod->getAttributes(); + $toAddAnnotations = array_merge($toAddAnnotations, array_map( + static function ($attribute) { + return $attribute->newInstance(); + }, + array_filter($attributes, static function ($annotation) use ($annotationClass): bool { + return is_a($annotation->getName(), $annotationClass, true); + }) + )); + } } catch (AnnotationException $e) { if ($this->mode === self::STRICT_MODE) { throw $e; diff --git a/src/Annotations/AbstractRequest.php b/src/Annotations/AbstractRequest.php index e25eecabfb..8addd9d0ba 100644 --- a/src/Annotations/AbstractRequest.php +++ b/src/Annotations/AbstractRequest.php @@ -15,10 +15,10 @@ abstract class AbstractRequest /** * @param mixed[] $attributes */ - public function __construct(array $attributes = []) + public function __construct(array $attributes = [], ?string $name = null, ?string $outputType = null) { - $this->outputType = $attributes['outputType'] ?? null; - $this->name = $attributes['name'] ?? null; + $this->outputType = $outputType ?? $attributes['outputType'] ?? null; + $this->name = $name ?? $attributes['name'] ?? null; } /** diff --git a/src/Annotations/Autowire.php b/src/Annotations/Autowire.php index ba43354596..0d4ac94af7 100644 --- a/src/Annotations/Autowire.php +++ b/src/Annotations/Autowire.php @@ -4,7 +4,10 @@ namespace TheCodingMachine\GraphQLite\Annotations; +use Attribute; use BadMethodCallException; + +use function is_string; use function ltrim; /** @@ -17,27 +20,35 @@ * @Attribute("identifier", type = "string") * }) */ +#[Attribute(Attribute::TARGET_PARAMETER)] class Autowire implements ParameterAnnotationInterface { - /** @var string */ + /** @var string|null */ private $for; /** @var string|null */ private $identifier; /** - * @param array $values + * @param array|string $identifier */ - public function __construct(array $values) + public function __construct($identifier = []) { - if (! isset($values['for'])) { - throw new BadMethodCallException('The @Autowire annotation must be passed a target. For instance: "@Autowire(for="$myService")"'); + $values = $identifier; + if (is_string($values)) { + $this->identifier = $values; + } else { + $this->identifier = $values['identifier'] ?? $values['value'] ?? null; + if (isset($values['for'])) { + $this->for = ltrim($values['for'], '$'); + } } - $this->for = ltrim($values['for'], '$'); - $this->identifier = $values['identifier'] ?? $values['value'] ?? null; } public function getTarget(): string { + if ($this->for === null) { + throw new BadMethodCallException('The @Autowire annotation must be passed a target. For instance: "@Autowire(for="$myService")"'); + } return $this->for; } diff --git a/src/Annotations/Decorate.php b/src/Annotations/Decorate.php index 859724532f..7ff3ef7a3e 100644 --- a/src/Annotations/Decorate.php +++ b/src/Annotations/Decorate.php @@ -4,8 +4,11 @@ namespace TheCodingMachine\GraphQLite\Annotations; +use Attribute; use BadMethodCallException; +use function is_string; + /** * Methods with this annotation are decorating an input type when the input type is resolved. * This is meant to be used only when the input type is provided by a third-party library and you want to modify it. @@ -16,22 +19,27 @@ * @Attribute("inputTypeName", type = "string"), * }) */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] class Decorate { /** @var string */ private $inputTypeName; /** - * @param array $values + * @param array|string $inputTypeName * * @throws BadMethodCallException */ - public function __construct(array $values) + public function __construct($inputTypeName = []) { - if (! isset($values['value']) && ! isset($values['inputTypeName'])) { + $values = $inputTypeName; + if (is_string($values)) { + $this->inputTypeName = $values; + } elseif (! isset($values['value']) && ! isset($values['inputTypeName'])) { throw new BadMethodCallException('The @Decorate annotation must be passed an input type. For instance: "@Decorate("MyInputType")"'); + } else { + $this->inputTypeName = $values['value'] ?? $values['inputTypeName']; } - $this->inputTypeName = $values['value'] ?? $values['inputTypeName']; } public function getInputTypeName(): string diff --git a/src/Annotations/EnumType.php b/src/Annotations/EnumType.php index 45cbfd33c9..3ce3e2dc10 100644 --- a/src/Annotations/EnumType.php +++ b/src/Annotations/EnumType.php @@ -4,6 +4,8 @@ namespace TheCodingMachine\GraphQLite\Annotations; +use Attribute; + /** * The EnumType annotation is useful to change the name of the generated "enum" type. * @@ -13,6 +15,7 @@ * @Attribute("name", type = "string"), * }) */ +#[Attribute(Attribute::TARGET_CLASS)] class EnumType { /** @var string|null */ @@ -21,9 +24,9 @@ class EnumType /** * @param mixed[] $attributes */ - public function __construct(array $attributes = []) + public function __construct(array $attributes = [], ?string $name = null) { - $this->name = $attributes['name'] ?? null; + $this->name = $name ?? $attributes['name'] ?? null; } /** diff --git a/src/Annotations/Exceptions/ClassNotFoundException.php b/src/Annotations/Exceptions/ClassNotFoundException.php index 36f8201beb..6cd1f9d6e4 100644 --- a/src/Annotations/Exceptions/ClassNotFoundException.php +++ b/src/Annotations/Exceptions/ClassNotFoundException.php @@ -5,6 +5,7 @@ namespace TheCodingMachine\GraphQLite\Annotations\Exceptions; use InvalidArgumentException; + use function sprintf; class ClassNotFoundException extends InvalidArgumentException diff --git a/src/Annotations/Exceptions/InvalidParameterException.php b/src/Annotations/Exceptions/InvalidParameterException.php index 8a1dd64092..91d31a7ad0 100644 --- a/src/Annotations/Exceptions/InvalidParameterException.php +++ b/src/Annotations/Exceptions/InvalidParameterException.php @@ -6,6 +6,7 @@ use BadMethodCallException; use ReflectionMethod; + use function sprintf; class InvalidParameterException extends BadMethodCallException diff --git a/src/Annotations/ExtendType.php b/src/Annotations/ExtendType.php index f7d2d21f3b..67ff4e715e 100644 --- a/src/Annotations/ExtendType.php +++ b/src/Annotations/ExtendType.php @@ -4,8 +4,10 @@ namespace TheCodingMachine\GraphQLite\Annotations; +use Attribute; use BadMethodCallException; use TheCodingMachine\GraphQLite\Annotations\Exceptions\ClassNotFoundException; + use function class_exists; use function interface_exists; use function ltrim; @@ -20,6 +22,7 @@ * @Attribute("name", type = "string"), * }) */ +#[Attribute(Attribute::TARGET_CLASS)] class ExtendType { /** @var class-string|null */ @@ -30,17 +33,18 @@ class ExtendType /** * @param mixed[] $attributes */ - public function __construct(array $attributes = []) + public function __construct(array $attributes = [], ?string $class = null, ?string $name = null) { - if (! isset($attributes['class']) && ! isset($attributes['name'])) { - throw new BadMethodCallException('In annotation @ExtendType, missing one of the compulsory parameter "class" or "name".'); + $className = isset($attributes['class']) ? ltrim($attributes['class'], '\\') : null; + $className = $className ?? $class; + if ($className !== null && ! class_exists($className) && ! interface_exists($className)) { + throw ClassNotFoundException::couldNotFindClass($className); } - $class = isset($attributes['class']) ? ltrim($attributes['class'], '\\') : null; - $this->name = $attributes['name'] ?? null; - if ($class !== null && ! class_exists($class) && ! interface_exists($class)) { - throw ClassNotFoundException::couldNotFindClass($class); + $this->name = $name ?? $attributes['name'] ?? null; + $this->class = $className; + if (! $this->class && ! $this->name) { + throw new BadMethodCallException('In annotation @ExtendType, missing one of the compulsory parameter "class" or "name".'); } - $this->class = $class; } /** diff --git a/src/Annotations/Factory.php b/src/Annotations/Factory.php index c142e9635b..39a7eff16d 100644 --- a/src/Annotations/Factory.php +++ b/src/Annotations/Factory.php @@ -4,6 +4,7 @@ namespace TheCodingMachine\GraphQLite\Annotations; +use Attribute; use TheCodingMachine\GraphQLite\GraphQLRuntimeException; /** @@ -16,6 +17,7 @@ * @Attribute("default", type = "bool") * }) */ +#[Attribute(Attribute::TARGET_METHOD)] class Factory { /** @var string|null */ @@ -26,11 +28,11 @@ class Factory /** * @param mixed[] $attributes */ - public function __construct(array $attributes = []) + public function __construct(array $attributes = [], ?string $name = null, ?bool $default = null) { - $this->name = $attributes['name'] ?? null; + $this->name = $name ?? $attributes['name'] ?? null; // This IS the default if no name is set and no "default" attribute is passed. - $this->default = $attributes['default'] ?? ! isset($attributes['name']); + $this->default = $default ?? $attributes['default'] ?? ! isset($attributes['name']); if ($this->name === null && $this->default === false) { throw new GraphQLRuntimeException('A @Factory that has "default=false" attribute must be given a name (i.e. add a name="FooBarInput" attribute).'); diff --git a/src/Annotations/FailWith.php b/src/Annotations/FailWith.php index 5eba69eb6d..fe2aa999c8 100644 --- a/src/Annotations/FailWith.php +++ b/src/Annotations/FailWith.php @@ -4,8 +4,11 @@ namespace TheCodingMachine\GraphQLite\Annotations; +use Attribute; use BadMethodCallException; + use function array_key_exists; +use function is_array; /** * @Annotation @@ -15,6 +18,7 @@ * @Attribute("mode", type = "string") * }) */ +#[Attribute(Attribute::TARGET_METHOD)] class FailWith implements MiddlewareAnnotationInterface { /** @@ -25,16 +29,20 @@ class FailWith implements MiddlewareAnnotationInterface private $value; /** - * @param array $values + * @param array|mixed $values + * @param mixed $value * * @throws BadMethodCallException */ - public function __construct(array $values) + public function __construct($values = [], $value = '__fail__with__magic__key__') { - if (! array_key_exists('value', $values)) { + if ($value !== '__fail__with__magic__key__') { + $this->value = $value; + } elseif (is_array($values) && array_key_exists('value', $values)) { + $this->value = $values['value']; + } else { throw new BadMethodCallException('The @FailWith annotation must be passed a defaultValue. For instance: "@FailWith(null)"'); } - $this->value = $values['value']; } /** diff --git a/src/Annotations/Field.php b/src/Annotations/Field.php index 8b4273c3e0..a3e059858a 100644 --- a/src/Annotations/Field.php +++ b/src/Annotations/Field.php @@ -4,6 +4,8 @@ namespace TheCodingMachine\GraphQLite\Annotations; +use Attribute; + /** * @Annotation * @Target({"METHOD"}) @@ -13,6 +15,7 @@ * @Attribute("prefetchMethod", type = "string"), * }) */ +#[Attribute(Attribute::TARGET_METHOD)] class Field extends AbstractRequest { /** @var string|null */ @@ -21,10 +24,10 @@ class Field extends AbstractRequest /** * @param mixed[] $attributes */ - public function __construct(array $attributes = []) + public function __construct(array $attributes = [], ?string $name = null, ?string $outputType = null, ?string $prefetchMethod = null) { - parent::__construct($attributes); - $this->prefetchMethod = $attributes['prefetchMethod'] ?? null; + parent::__construct($attributes, $name, $outputType); + $this->prefetchMethod = $prefetchMethod ?? $attributes['prefetchMethod'] ?? null; } /** diff --git a/src/Annotations/HideIfUnauthorized.php b/src/Annotations/HideIfUnauthorized.php index 9e84283f60..1ef71a2b66 100644 --- a/src/Annotations/HideIfUnauthorized.php +++ b/src/Annotations/HideIfUnauthorized.php @@ -4,6 +4,8 @@ namespace TheCodingMachine\GraphQLite\Annotations; +use Attribute; + /** * Fields/Queries/Mutations annotated with this annotation will be hidden from the schema if the user is not logged * or has no right associated. @@ -11,6 +13,7 @@ * @Annotation * @Target({"METHOD", "ANNOTATION"}) */ +#[Attribute(Attribute::TARGET_METHOD)] class HideIfUnauthorized implements MiddlewareAnnotationInterface { } diff --git a/src/Annotations/HideParameter.php b/src/Annotations/HideParameter.php index dfa3c947a9..da8ddcd170 100644 --- a/src/Annotations/HideParameter.php +++ b/src/Annotations/HideParameter.php @@ -5,6 +5,7 @@ namespace TheCodingMachine\GraphQLite\Annotations; use BadMethodCallException; + use function ltrim; /** @@ -25,16 +26,20 @@ class HideParameter implements ParameterAnnotationInterface /** * @param array $values */ - public function __construct(array $values) + public function __construct(array $values = []) { if (! isset($values['for'])) { - throw new BadMethodCallException('The @HideParameter annotation must be passed a target. For instance: "@HideParameter(for="$myParameterToHide")"'); + return; } + $this->for = ltrim($values['for'], '$'); } public function getTarget(): string { + if ($this->for === null) { + throw new BadMethodCallException('The @HideParameter annotation must be passed a target. For instance: "@HideParameter(for="$myParameterToHide")"'); + } return $this->for; } } diff --git a/src/Annotations/InjectUser.php b/src/Annotations/InjectUser.php index f8651aee8c..5ef0a7842e 100644 --- a/src/Annotations/InjectUser.php +++ b/src/Annotations/InjectUser.php @@ -5,6 +5,7 @@ namespace TheCodingMachine\GraphQLite\Annotations; use BadMethodCallException; + use function ltrim; /** @@ -25,16 +26,20 @@ class InjectUser implements ParameterAnnotationInterface /** * @param array $values */ - public function __construct(array $values) + public function __construct(array $values = []) { if (! isset($values['for'])) { - throw new BadMethodCallException('The @InjectUser annotation must be passed a target. For instance: "@InjectUser(for="$user")"'); + return; } + $this->for = ltrim($values['for'], '$'); } public function getTarget(): string { + if ($this->for === null) { + throw new BadMethodCallException('The @InjectUser annotation must be passed a target. For instance: "@InjectUser(for="$user")"'); + } return $this->for; } } diff --git a/src/Annotations/Logged.php b/src/Annotations/Logged.php index b21364f174..631b98034c 100644 --- a/src/Annotations/Logged.php +++ b/src/Annotations/Logged.php @@ -4,10 +4,13 @@ namespace TheCodingMachine\GraphQLite\Annotations; +use Attribute; + /** * @Annotation * @Target({"METHOD", "ANNOTATION"}) */ +#[Attribute(Attribute::TARGET_METHOD)] class Logged implements MiddlewareAnnotationInterface { } diff --git a/src/Annotations/MagicField.php b/src/Annotations/MagicField.php index a410152e6b..2a5b041033 100644 --- a/src/Annotations/MagicField.php +++ b/src/Annotations/MagicField.php @@ -4,7 +4,9 @@ namespace TheCodingMachine\GraphQLite\Annotations; +use Attribute; use BadMethodCallException; + use function array_map; use function is_array; @@ -20,6 +22,7 @@ * @Attribute("annotations", type = "mixed"), * }) */ +#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] class MagicField implements SourceFieldInterface { /** @var string */ @@ -40,22 +43,22 @@ class MagicField implements SourceFieldInterface /** * @param mixed[] $attributes */ - public function __construct(array $attributes = []) + public function __construct(array $attributes = [], ?string $name = null, ?string $outputType = null, ?string $phpType = null) { - if (! isset($attributes['name']) || (! isset($attributes['outputType']) && ! isset($attributes['phpType']))) { + $this->name = $attributes['name'] ?? $name; + $this->outputType = $attributes['outputType'] ?? $outputType ?? null; + $this->phpType = $attributes['phpType'] ?? $phpType ?? null; + if (! $this->name || (! $this->outputType && ! $this->phpType)) { throw new BadMethodCallException('The @MagicField annotation must be passed a name and an output type or a php type. For instance: "@MagicField(name=\'phone\', outputType=\'String!\')" or "@MagicField(name=\'phone\', phpType=\'string\')"'); } - if (isset($attributes['outputType']) && isset($attributes['phpType'])) { + if (isset($this->outputType) && $this->phpType) { throw new BadMethodCallException('In a @MagicField annotation, you cannot use the outputType and the phpType at the same time. For instance: "@MagicField(name=\'phone\', outputType=\'String!\')" or "@MagicField(name=\'phone\', phpType=\'string\')"'); } - $this->name = $attributes['name']; - $this->outputType = $attributes['outputType'] ?? null; - $this->phpType = $attributes['phpType'] ?? null; $middlewareAnnotations = []; $parameterAnnotations = []; $annotations = $attributes['annotations'] ?? []; if (! is_array($annotations)) { - $annotations = [ $annotations ]; + $annotations = [$annotations]; } foreach ($annotations ?? [] as $annotation) { if ($annotation instanceof MiddlewareAnnotationInterface) { diff --git a/src/Annotations/Mutation.php b/src/Annotations/Mutation.php index 86d6948659..060ee146b2 100644 --- a/src/Annotations/Mutation.php +++ b/src/Annotations/Mutation.php @@ -4,6 +4,8 @@ namespace TheCodingMachine\GraphQLite\Annotations; +use Attribute; + /** * @Annotation * @Target({"METHOD"}) @@ -11,6 +13,7 @@ * @Attribute("outputType", type = "string"), * }) */ +#[Attribute(Attribute::TARGET_METHOD)] class Mutation extends AbstractRequest { } diff --git a/src/Annotations/Query.php b/src/Annotations/Query.php index a8cea2de4d..99ac7a3a5d 100644 --- a/src/Annotations/Query.php +++ b/src/Annotations/Query.php @@ -4,6 +4,8 @@ namespace TheCodingMachine\GraphQLite\Annotations; +use Attribute; + /** * @Annotation * @Target({"METHOD"}) @@ -11,6 +13,7 @@ * @Attribute("outputType", type = "string"), * }) */ +#[Attribute(Attribute::TARGET_METHOD)] class Query extends AbstractRequest { } diff --git a/src/Annotations/Right.php b/src/Annotations/Right.php index db691a9581..f7e2faabbf 100644 --- a/src/Annotations/Right.php +++ b/src/Annotations/Right.php @@ -4,8 +4,11 @@ namespace TheCodingMachine\GraphQLite\Annotations; +use Attribute; use BadMethodCallException; +use function is_string; + /** * @Annotation * @Target({"ANNOTATION", "METHOD"}) @@ -13,22 +16,27 @@ * @Attribute("name", type = "string"), * }) */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] class Right implements MiddlewareAnnotationInterface { /** @var string */ private $name; /** - * @param array $values + * @param array|string $name * * @throws BadMethodCallException */ - public function __construct(array $values) + public function __construct($name = []) { - if (! isset($values['value']) && ! isset($values['name'])) { + $data = $name; + if (is_string($data)) { + $data = ['name' => $data]; + } + if (! isset($data['value']) && ! isset($data['name'])) { throw new BadMethodCallException('The @Right annotation must be passed a right name. For instance: "@Right(\'my_right\')"'); } - $this->name = $values['value'] ?? $values['name']; + $this->name = $data['value'] ?? $data['name']; } public function getName(): string diff --git a/src/Annotations/Security.php b/src/Annotations/Security.php index e4a4c21a70..f4c7ae8f2b 100644 --- a/src/Annotations/Security.php +++ b/src/Annotations/Security.php @@ -4,8 +4,15 @@ namespace TheCodingMachine\GraphQLite\Annotations; +use Attribute; use BadMethodCallException; +use TypeError; + use function array_key_exists; +use function gettype; +use function is_array; +use function is_string; +use function sprintf; /** * @Annotation @@ -17,6 +24,7 @@ * @Attribute("message", type = "string"), * }) */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] class Security implements MiddlewareAnnotationInterface { /** @var string */ @@ -31,23 +39,34 @@ class Security implements MiddlewareAnnotationInterface private $message; /** - * @param array $values + * @param array|string $data data array managed by the Doctrine Annotations library or the expression + * @param mixed $failWith * * @throws BadMethodCallException */ - public function __construct(array $values) + public function __construct($data = [], ?string $expression = null, $failWith = '__fail__with__magic__key__', ?string $message = null, ?int $statusCode = null) { - if (! isset($values['value']) && ! isset($values['expression'])) { + if (is_string($data)) { + $data = ['expression' => $data]; + } elseif (! is_array($data)) { + throw new TypeError(sprintf('"%s": Argument $data is expected to be a string or array, got "%s".', __METHOD__, gettype($data))); + } + + $this->expression = $data['value'] ?? $data['expression'] ?? $expression; + if (! $this->expression) { throw new BadMethodCallException('The @Security annotation must be passed an expression. For instance: "@Security("is_granted(\'CAN_EDIT_STUFF\')")"'); } - $this->expression = $values['value'] ?? $values['expression']; - if (array_key_exists('failWith', $values)) { - $this->failWith = $values['failWith']; + + if (array_key_exists('failWith', $data)) { + $this->failWith = $data['failWith']; + $this->failWithIsSet = true; + } elseif ($failWith !== '__fail__with__magic__key__') { + $this->failWith = $failWith; $this->failWithIsSet = true; } - $this->message = $values['message'] ?? 'Access denied.'; - $this->statusCode = $values['statusCode'] ?? 403; - if ($this->failWithIsSet === true && (isset($values['message']) || isset($values['statusCode']))) { + $this->message = $message ?? $data['message'] ?? 'Access denied.'; + $this->statusCode = $statusCode ?? $data['statusCode'] ?? 403; + if ($this->failWithIsSet === true && (($message || isset($data['message'])) || ($statusCode || isset($data['statusCode'])))) { throw new BadMethodCallException('A @Security annotation that has "failWith" attribute set cannot have a message or a statusCode attribute.'); } } diff --git a/src/Annotations/SourceField.php b/src/Annotations/SourceField.php index 94d17c1ef5..46652ee6f5 100644 --- a/src/Annotations/SourceField.php +++ b/src/Annotations/SourceField.php @@ -4,7 +4,9 @@ namespace TheCodingMachine\GraphQLite\Annotations; +use Attribute; use BadMethodCallException; + use function array_map; use function is_array; @@ -20,6 +22,7 @@ * @Attribute("annotations", type = "mixed"), * }) */ +#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] class SourceField implements SourceFieldInterface { /** @var string */ @@ -40,22 +43,24 @@ class SourceField implements SourceFieldInterface /** * @param mixed[] $attributes */ - public function __construct(array $attributes = []) + public function __construct(array $attributes = [], ?string $name = null, ?string $outputType = null, ?string $phpType = null) { - if (! isset($attributes['name'])) { + $name = $name ?? $attributes['name'] ?? null; + if ($name === null) { throw new BadMethodCallException('The @SourceField annotation must be passed a name. For instance: "@SourceField(name=\'phone\')"'); } - if (isset($attributes['outputType']) && isset($attributes['phpType'])) { + $this->name = $name; + + $this->outputType = $outputType ?? $attributes['outputType'] ?? null; + $this->phpType = $phpType ?? $attributes['phpType'] ?? null; + if ($this->outputType && $this->phpType) { throw new BadMethodCallException('In a @SourceField annotation, you cannot use the outputType and the phpType at the same time. For instance: "@SourceField(name=\'phone\', outputType=\'String!\')" or "@SourceField(name=\'phone\', phpType=\'string\')"'); } - $this->name = $attributes['name']; - $this->outputType = $attributes['outputType'] ?? null; - $this->phpType = $attributes['phpType'] ?? null; $middlewareAnnotations = []; $parameterAnnotations = []; $annotations = $attributes['annotations'] ?? []; if (! is_array($annotations)) { - $annotations = [ $annotations ]; + $annotations = [$annotations]; } foreach ($annotations ?? [] as $annotation) { if ($annotation instanceof MiddlewareAnnotationInterface) { diff --git a/src/Annotations/Type.php b/src/Annotations/Type.php index 4a6dd23611..6d1c3d1ed0 100644 --- a/src/Annotations/Type.php +++ b/src/Annotations/Type.php @@ -4,9 +4,11 @@ namespace TheCodingMachine\GraphQLite\Annotations; +use Attribute; use RuntimeException; use TheCodingMachine\GraphQLite\Annotations\Exceptions\ClassNotFoundException; use TheCodingMachine\GraphQLite\GraphQLRuntimeException; + use function class_exists; use function interface_exists; use function ltrim; @@ -24,6 +26,7 @@ * @Attribute("external", type = "bool"), * }) */ +#[Attribute(Attribute::TARGET_CLASS)] class Type { /** @var class-string|null */ @@ -44,20 +47,22 @@ class Type /** * @param mixed[] $attributes + * @param class-string|null $class */ - public function __construct(array $attributes = []) + public function __construct(array $attributes = [], ?string $class = null, ?string $name = null, ?bool $default = null, ?bool $external = null) { - $external = $attributes['external'] ?? null; - if (isset($attributes['class'])) { - $this->setClass($attributes['class']); + $external = $external ?? $attributes['external'] ?? null; + $class = $class ?? $attributes['class'] ?? null; + if ($class !== null) { + $this->setClass($class); } else { $this->selfType = true; } - $this->name = $attributes['name'] ?? null; + $this->name = $name ?? $attributes['name'] ?? null; // If no value is passed for default, "default" = true - $this->default = $attributes['default'] ?? true; + $this->default = $default ?? $attributes['default'] ?? true; if ($external === null) { return; diff --git a/src/Annotations/UseInputType.php b/src/Annotations/UseInputType.php index ee5b06d79f..1c27b601e7 100644 --- a/src/Annotations/UseInputType.php +++ b/src/Annotations/UseInputType.php @@ -5,6 +5,8 @@ namespace TheCodingMachine\GraphQLite\Annotations; use BadMethodCallException; + +use function is_string; use function ltrim; /** @@ -19,27 +21,38 @@ */ class UseInputType implements ParameterAnnotationInterface { - /** @var string */ + /** @var string|null */ private $for; /** @var string */ private $inputType; /** - * @param array $values + * @param array|string $inputType * * @throws BadMethodCallException */ - public function __construct(array $values) + public function __construct($inputType = []) { - if (! isset($values['for'], $values['inputType'])) { - throw new BadMethodCallException('The @UseInputType annotation must be passed a target and an input type. For instance: "@UseInputType(for="$input", inputType="MyInputType")"'); + $values = $inputType; + if (is_string($values)) { + $values = ['inputType' => $values]; + } + if (! isset($values['inputType'])) { + throw new BadMethodCallException('The @UseInputType annotation must be passed an input type. For instance: "@UseInputType(for="$input", inputType="MyInputType")" in PHP 7+ or #[UseInputType("MyInputType")] in PHP 8+'); } - $this->for = ltrim($values['for'], '$'); $this->inputType = $values['inputType']; + if (! isset($values['for'])) { + return; + } + + $this->for = ltrim($values['for'], '$'); } public function getTarget(): string { + if ($this->for === null) { + throw new BadMethodCallException('The @UseInputType annotation must be passed a target and an input type. For instance: "@UseInputType(for="$input", inputType="MyInputType")" in PHP 7+ or #[UseInputType("MyInputType")] in PHP 8+'); + } return $this->for; } diff --git a/src/Containers/BasicAutoWiringContainer.php b/src/Containers/BasicAutoWiringContainer.php index 0f71095fde..955c64eb2a 100644 --- a/src/Containers/BasicAutoWiringContainer.php +++ b/src/Containers/BasicAutoWiringContainer.php @@ -9,6 +9,7 @@ use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; use ReflectionClass; + use function class_exists; /** diff --git a/src/Exceptions/GraphQLAggregateException.php b/src/Exceptions/GraphQLAggregateException.php index b06dd9f597..ef39eb4136 100644 --- a/src/Exceptions/GraphQLAggregateException.php +++ b/src/Exceptions/GraphQLAggregateException.php @@ -7,6 +7,7 @@ use Exception; use GraphQL\Error\ClientAware; use Throwable; + use function array_map; use function assert; use function count; diff --git a/src/Exceptions/WebonyxErrorHandler.php b/src/Exceptions/WebonyxErrorHandler.php index bfbe8a2bf5..b2fdc236c5 100644 --- a/src/Exceptions/WebonyxErrorHandler.php +++ b/src/Exceptions/WebonyxErrorHandler.php @@ -7,6 +7,7 @@ use GraphQL\Error\ClientAware; use GraphQL\Error\Error; use GraphQL\Error\FormattedError; + use function array_map; use function array_merge; diff --git a/src/FieldNotFoundException.php b/src/FieldNotFoundException.php index 9670500aea..c6d0001f2c 100644 --- a/src/FieldNotFoundException.php +++ b/src/FieldNotFoundException.php @@ -5,6 +5,7 @@ namespace TheCodingMachine\GraphQLite; use RuntimeException; + use function sprintf; use function ucfirst; diff --git a/src/FieldsBuilder.php b/src/FieldsBuilder.php index 7e53822e9e..ce0ae6080c 100644 --- a/src/FieldsBuilder.php +++ b/src/FieldsBuilder.php @@ -12,6 +12,7 @@ use ReflectionException; use ReflectionMethod; use ReflectionParameter; +use TheCodingMachine\GraphQLite\Annotations\AbstractRequest; use TheCodingMachine\GraphQLite\Annotations\Exceptions\InvalidParameterException; use TheCodingMachine\GraphQLite\Annotations\Field; use TheCodingMachine\GraphQLite\Annotations\Mutation; @@ -33,6 +34,7 @@ use TheCodingMachine\GraphQLite\Types\MutableObjectType; use TheCodingMachine\GraphQLite\Types\TypeResolver; use Webmozart\Assert\Assert; + use function array_merge; use function array_shift; use function assert; @@ -199,6 +201,7 @@ public function getParametersForDecorator(ReflectionMethod $refMethod): array /** * @param object|class-string $controller The controller instance, or the name of the source class name + * @param class-string $annotationName * @param bool $injectSource Whether to inject the source object or not as the first argument. True for @Field (unless @Type has no class attribute), false for @Query and @Mutation * * @return array diff --git a/src/GlobControllerQueryProvider.php b/src/GlobControllerQueryProvider.php index c491904daf..4d9da17fef 100644 --- a/src/GlobControllerQueryProvider.php +++ b/src/GlobControllerQueryProvider.php @@ -16,6 +16,7 @@ use TheCodingMachine\GraphQLite\Annotations\Mutation; use TheCodingMachine\GraphQLite\Annotations\Query; use Webmozart\Assert\Assert; + use function class_exists; use function interface_exists; use function str_replace; diff --git a/src/Http/HttpCodeDecider.php b/src/Http/HttpCodeDecider.php index c507d16b9b..658b6f0c15 100644 --- a/src/Http/HttpCodeDecider.php +++ b/src/Http/HttpCodeDecider.php @@ -6,6 +6,7 @@ use GraphQL\Error\ClientAware; use GraphQL\Executor\ExecutionResult; + use function max; class HttpCodeDecider implements HttpCodeDeciderInterface diff --git a/src/Http/Psr15GraphQLMiddlewareBuilder.php b/src/Http/Psr15GraphQLMiddlewareBuilder.php index f83927fa42..d48dd858d6 100644 --- a/src/Http/Psr15GraphQLMiddlewareBuilder.php +++ b/src/Http/Psr15GraphQLMiddlewareBuilder.php @@ -15,6 +15,7 @@ use TheCodingMachine\GraphQLite\Context\Context; use TheCodingMachine\GraphQLite\Exceptions\WebonyxErrorHandler; use TheCodingMachine\GraphQLite\GraphQLRuntimeException; + use function class_exists; /** diff --git a/src/Http/WebonyxGraphqlMiddleware.php b/src/Http/WebonyxGraphqlMiddleware.php index d87eec2aba..9d55df807f 100644 --- a/src/Http/WebonyxGraphqlMiddleware.php +++ b/src/Http/WebonyxGraphqlMiddleware.php @@ -17,6 +17,7 @@ use Psr\Http\Server\RequestHandlerInterface; use RuntimeException; use TheCodingMachine\GraphQLite\Context\ResetableContextInterface; + use function array_map; use function explode; use function in_array; @@ -26,6 +27,7 @@ use function json_last_error; use function json_last_error_msg; use function max; + use const JSON_ERROR_NONE; final class WebonyxGraphqlMiddleware implements MiddlewareInterface diff --git a/src/InputTypeGenerator.php b/src/InputTypeGenerator.php index 7bcc538803..0edd15f129 100644 --- a/src/InputTypeGenerator.php +++ b/src/InputTypeGenerator.php @@ -11,6 +11,7 @@ use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputInterface; use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputObjectType; use Webmozart\Assert\Assert; + use function array_shift; /** diff --git a/src/InputTypeUtils.php b/src/InputTypeUtils.php index 1763560d73..88028dad5c 100644 --- a/src/InputTypeUtils.php +++ b/src/InputTypeUtils.php @@ -17,6 +17,7 @@ use TheCodingMachine\GraphQLite\Parameters\InputTypeParameterInterface; use TheCodingMachine\GraphQLite\Parameters\ParameterInterface; use Webmozart\Assert\Assert; + use function array_filter; use function array_map; use function assert; diff --git a/src/InvalidPrefetchMethodRuntimeException.php b/src/InvalidPrefetchMethodRuntimeException.php index 97ee7916ea..5deb51e46c 100644 --- a/src/InvalidPrefetchMethodRuntimeException.php +++ b/src/InvalidPrefetchMethodRuntimeException.php @@ -20,6 +20,6 @@ public static function methodNotFound(ReflectionMethod $annotationMethod, Reflec public static function prefetchDataIgnored(ReflectionMethod $annotationMethod, bool $isSecond): self { - throw new self('The @Field annotation in ' . $annotationMethod->getDeclaringClass()->getName() . '::' . $annotationMethod->getName() . ' specifies a "prefetch method" but the data from the prefetch method is not gathered. The "' . $annotationMethod->getName() . '" method should accept a ' . ($isSecond?'second':'first') . ' parameter that will contain data returned by the prefetch method.'); + throw new self('The @Field annotation in ' . $annotationMethod->getDeclaringClass()->getName() . '::' . $annotationMethod->getName() . ' specifies a "prefetch method" but the data from the prefetch method is not gathered. The "' . $annotationMethod->getName() . '" method should accept a ' . ($isSecond ? 'second' : 'first') . ' parameter that will contain data returned by the prefetch method.'); } } diff --git a/src/Mappers/CannotMapTypeException.php b/src/Mappers/CannotMapTypeException.php index 8114c0159a..79d74de35f 100644 --- a/src/Mappers/CannotMapTypeException.php +++ b/src/Mappers/CannotMapTypeException.php @@ -17,6 +17,7 @@ use phpDocumentor\Reflection\Types\Mixed_; use ReflectionMethod; use TheCodingMachine\GraphQLite\Annotations\ExtendType; + use function array_map; use function assert; use function implode; diff --git a/src/Mappers/CannotMapTypeTrait.php b/src/Mappers/CannotMapTypeTrait.php index 4799a48087..04e942c636 100644 --- a/src/Mappers/CannotMapTypeTrait.php +++ b/src/Mappers/CannotMapTypeTrait.php @@ -10,6 +10,7 @@ use TheCodingMachine\GraphQLite\Annotations\ExtendType; use TheCodingMachine\GraphQLite\Annotations\SourceFieldInterface; use Webmozart\Assert\Assert; + use function sprintf; trait CannotMapTypeTrait diff --git a/src/Mappers/CompositeTypeMapper.php b/src/Mappers/CompositeTypeMapper.php index 2eea77af56..eea9ca476c 100644 --- a/src/Mappers/CompositeTypeMapper.php +++ b/src/Mappers/CompositeTypeMapper.php @@ -12,6 +12,7 @@ use TheCodingMachine\GraphQLite\Types\MutableInterfaceType; use TheCodingMachine\GraphQLite\Types\MutableObjectType; use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputInterface; + use function array_map; use function array_merge; use function array_unique; diff --git a/src/Mappers/DuplicateMappingException.php b/src/Mappers/DuplicateMappingException.php index 40427a523b..ae60f4656b 100644 --- a/src/Mappers/DuplicateMappingException.php +++ b/src/Mappers/DuplicateMappingException.php @@ -6,6 +6,7 @@ use ReflectionMethod; use RuntimeException; + use function sprintf; class DuplicateMappingException extends RuntimeException diff --git a/src/Mappers/GlobTypeMapper.php b/src/Mappers/GlobTypeMapper.php index 9c24af4012..9c123e07b6 100644 --- a/src/Mappers/GlobTypeMapper.php +++ b/src/Mappers/GlobTypeMapper.php @@ -13,6 +13,7 @@ use TheCodingMachine\GraphQLite\NamingStrategyInterface; use TheCodingMachine\GraphQLite\TypeGenerator; use TheCodingMachine\GraphQLite\Utils\Namespaces\NS; + use function str_replace; /** diff --git a/src/Mappers/GlobTypeMapperCache.php b/src/Mappers/GlobTypeMapperCache.php index a19e088f44..af54a9e373 100644 --- a/src/Mappers/GlobTypeMapperCache.php +++ b/src/Mappers/GlobTypeMapperCache.php @@ -5,6 +5,7 @@ namespace TheCodingMachine\GraphQLite\Mappers; use ReflectionClass; + use function array_keys; /** diff --git a/src/Mappers/Parameters/ContainerParameterHandler.php b/src/Mappers/Parameters/ContainerParameterHandler.php index 5dd2ca0d36..fe9f5099fb 100644 --- a/src/Mappers/Parameters/ContainerParameterHandler.php +++ b/src/Mappers/Parameters/ContainerParameterHandler.php @@ -13,6 +13,7 @@ use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotations; use TheCodingMachine\GraphQLite\Parameters\ContainerParameter; use TheCodingMachine\GraphQLite\Parameters\ParameterInterface; + use function assert; /** diff --git a/src/Mappers/Parameters/Next.php b/src/Mappers/Parameters/Next.php index a141b6b884..fd29fb8bbd 100644 --- a/src/Mappers/Parameters/Next.php +++ b/src/Mappers/Parameters/Next.php @@ -10,6 +10,7 @@ use SplQueue; use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotations; use TheCodingMachine\GraphQLite\Parameters\ParameterInterface; + use function assert; /** diff --git a/src/Mappers/Parameters/ResolveInfoParameterHandler.php b/src/Mappers/Parameters/ResolveInfoParameterHandler.php index a82a0409d0..38cbd3e840 100644 --- a/src/Mappers/Parameters/ResolveInfoParameterHandler.php +++ b/src/Mappers/Parameters/ResolveInfoParameterHandler.php @@ -12,6 +12,7 @@ use TheCodingMachine\GraphQLite\Annotations\ParameterAnnotations; use TheCodingMachine\GraphQLite\Parameters\ParameterInterface; use TheCodingMachine\GraphQLite\Parameters\ResolveInfoParameter; + use function assert; class ResolveInfoParameterHandler implements ParameterMiddlewareInterface @@ -20,7 +21,7 @@ public function mapParameter(ReflectionParameter $parameter, DocBlock $docBlock, { $type = $parameter->getType(); assert($type === null || $type instanceof ReflectionNamedType); - if ($type!== null && $type->getName() === ResolveInfo::class) { + if ($type !== null && $type->getName() === ResolveInfo::class) { return new ResolveInfoParameter(); } diff --git a/src/Mappers/Parameters/TypeHandler.php b/src/Mappers/Parameters/TypeHandler.php index f67f491e90..893cca883f 100644 --- a/src/Mappers/Parameters/TypeHandler.php +++ b/src/Mappers/Parameters/TypeHandler.php @@ -38,11 +38,13 @@ use TheCodingMachine\GraphQLite\Types\ArgumentResolver; use TheCodingMachine\GraphQLite\Types\TypeResolver; use Webmozart\Assert\Assert; + use function array_merge; use function array_unique; use function assert; use function count; use function iterator_to_array; + use const SORT_REGULAR; class TypeHandler implements ParameterHandlerInterface @@ -210,7 +212,7 @@ private function appendTypes(Type $type, ?Type $docBlockType): Type return $type; } - $types = [ $type ]; + $types = [$type]; if ($docBlockType instanceof Compound) { $docBlockTypes = iterator_to_array($docBlockType); $types = array_merge($types, $docBlockTypes); diff --git a/src/Mappers/PorpaginasTypeMapper.php b/src/Mappers/PorpaginasTypeMapper.php index 30dc73a82c..9d4d5a635d 100644 --- a/src/Mappers/PorpaginasTypeMapper.php +++ b/src/Mappers/PorpaginasTypeMapper.php @@ -15,6 +15,7 @@ use TheCodingMachine\GraphQLite\Types\MutableInterfaceType; use TheCodingMachine\GraphQLite\Types\MutableObjectType; use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputInterface; + use function get_class; use function is_a; use function strpos; diff --git a/src/Mappers/RecursiveTypeMapper.php b/src/Mappers/RecursiveTypeMapper.php index a53d4d242e..4f59b94d64 100644 --- a/src/Mappers/RecursiveTypeMapper.php +++ b/src/Mappers/RecursiveTypeMapper.php @@ -20,7 +20,9 @@ use TheCodingMachine\GraphQLite\Types\MutableObjectType; use TheCodingMachine\GraphQLite\Types\ObjectFromInterfaceType; use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputInterface; +use TypeError; use Webmozart\Assert\Assert; + use function array_flip; use function array_reverse; use function class_implements; @@ -175,7 +177,11 @@ public function findClosestMatchingParent(string $className): ?string if ($this->typeMapper->canMapClassToType($className)) { return $className; } - $className = get_parent_class($className); + try { + $className = get_parent_class($className); + } catch (TypeError $exception) { + return null; + } } while ($className); return null; @@ -357,7 +363,7 @@ private function getMappedClass(string $className, array $supportedClasses): Map $mappedClass = new MappedClass(/*$className*/); $this->mappedClasses[$className] = $mappedClass; $parentClassName = $className; - foreach (class_implements($className) as $interfaceName) { + foreach (class_implements($className) ?: [] as $interfaceName) { if (! isset($supportedClasses[$interfaceName])) { continue; } diff --git a/src/Mappers/Root/BaseTypeMapper.php b/src/Mappers/Root/BaseTypeMapper.php index bf9960e91d..888e62a516 100644 --- a/src/Mappers/Root/BaseTypeMapper.php +++ b/src/Mappers/Root/BaseTypeMapper.php @@ -29,6 +29,7 @@ use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface; use TheCodingMachine\GraphQLite\Types\DateTimeType; use TheCodingMachine\GraphQLite\Types\ID; + use function ltrim; /** @@ -141,12 +142,16 @@ private function mapBaseType(Type $type) case '\\DateTimeImmutable': case '\\DateTimeInterface': return self::getDateTimeType(); + case '\\' . UploadedFileInterface::class: return self::getUploadType(); + case '\\DateTime': throw CannotMapTypeException::createForDateTime(); + case '\\' . ID::class: return GraphQLType::id(); + default: return null; } diff --git a/src/Mappers/Root/CompoundTypeMapper.php b/src/Mappers/Root/CompoundTypeMapper.php index efca071471..d494b2258a 100644 --- a/src/Mappers/Root/CompoundTypeMapper.php +++ b/src/Mappers/Root/CompoundTypeMapper.php @@ -22,6 +22,7 @@ use TheCodingMachine\GraphQLite\TypeRegistry; use TheCodingMachine\GraphQLite\Types\UnionType; use Webmozart\Assert\Assert; + use function array_filter; use function array_values; use function assert; diff --git a/src/Mappers/Root/IteratorTypeMapper.php b/src/Mappers/Root/IteratorTypeMapper.php index 38933590cd..98447df0fa 100644 --- a/src/Mappers/Root/IteratorTypeMapper.php +++ b/src/Mappers/Root/IteratorTypeMapper.php @@ -23,6 +23,7 @@ use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeExceptionInterface; use Webmozart\Assert\Assert; + use function assert; use function count; use function iterator_to_array; diff --git a/src/Mappers/Root/MyCLabsEnumTypeMapper.php b/src/Mappers/Root/MyCLabsEnumTypeMapper.php index f0151003bf..e2ec3178b8 100644 --- a/src/Mappers/Root/MyCLabsEnumTypeMapper.php +++ b/src/Mappers/Root/MyCLabsEnumTypeMapper.php @@ -19,6 +19,7 @@ use TheCodingMachine\GraphQLite\AnnotationReader; use TheCodingMachine\GraphQLite\Types\MyCLabsEnumType; use TheCodingMachine\GraphQLite\Utils\Namespaces\NS; + use function assert; use function is_a; use function ltrim; diff --git a/src/Mappers/Root/NullableTypeMapperAdapter.php b/src/Mappers/Root/NullableTypeMapperAdapter.php index 6ac6fea7a2..cf9c8cf552 100644 --- a/src/Mappers/Root/NullableTypeMapperAdapter.php +++ b/src/Mappers/Root/NullableTypeMapperAdapter.php @@ -18,6 +18,7 @@ use ReflectionMethod; use TheCodingMachine\GraphQLite\Mappers\CannotMapTypeException; use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputObjectType; + use function array_filter; use function array_map; use function array_values; diff --git a/src/Mappers/StaticClassListTypeMapper.php b/src/Mappers/StaticClassListTypeMapper.php index 80f692b335..746f35fc41 100644 --- a/src/Mappers/StaticClassListTypeMapper.php +++ b/src/Mappers/StaticClassListTypeMapper.php @@ -13,6 +13,7 @@ use TheCodingMachine\GraphQLite\InputTypeUtils; use TheCodingMachine\GraphQLite\NamingStrategyInterface; use TheCodingMachine\GraphQLite\TypeGenerator; + use function class_exists; use function implode; use function interface_exists; diff --git a/src/Mappers/StaticTypeMapper.php b/src/Mappers/StaticTypeMapper.php index 46292cadc5..b66f646e63 100644 --- a/src/Mappers/StaticTypeMapper.php +++ b/src/Mappers/StaticTypeMapper.php @@ -16,6 +16,7 @@ use TheCodingMachine\GraphQLite\Types\MutableInterfaceType; use TheCodingMachine\GraphQLite\Types\MutableObjectType; use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputInterface; + use function array_keys; use function array_reduce; diff --git a/src/Middlewares/AuthorizationFieldMiddleware.php b/src/Middlewares/AuthorizationFieldMiddleware.php index 562d04b7af..478b5d3ad3 100644 --- a/src/Middlewares/AuthorizationFieldMiddleware.php +++ b/src/Middlewares/AuthorizationFieldMiddleware.php @@ -17,6 +17,7 @@ use TheCodingMachine\GraphQLite\Security\AuthenticationServiceInterface; use TheCodingMachine\GraphQLite\Security\AuthorizationServiceInterface; use Webmozart\Assert\Assert; + use function assert; /** diff --git a/src/Middlewares/MagicPropertyResolver.php b/src/Middlewares/MagicPropertyResolver.php index 7eeb71c0f9..3804ce6ad9 100644 --- a/src/Middlewares/MagicPropertyResolver.php +++ b/src/Middlewares/MagicPropertyResolver.php @@ -6,6 +6,7 @@ use TheCodingMachine\GraphQLite\GraphQLRuntimeException; use Webmozart\Assert\Assert; + use function get_class; use function is_object; use function method_exists; diff --git a/src/Middlewares/SecurityFieldMiddleware.php b/src/Middlewares/SecurityFieldMiddleware.php index a61bf23b10..a423d4a301 100644 --- a/src/Middlewares/SecurityFieldMiddleware.php +++ b/src/Middlewares/SecurityFieldMiddleware.php @@ -17,6 +17,7 @@ use TheCodingMachine\GraphQLite\Security\AuthorizationServiceInterface; use Throwable; use Webmozart\Assert\Assert; + use function array_combine; use function array_keys; use function assert; diff --git a/src/Middlewares/ServiceResolver.php b/src/Middlewares/ServiceResolver.php index fc97f6b8ac..a18fb7bb3e 100644 --- a/src/Middlewares/ServiceResolver.php +++ b/src/Middlewares/ServiceResolver.php @@ -5,7 +5,6 @@ namespace TheCodingMachine\GraphQLite\Middlewares; use function get_class; -use function is_object; /** * A class that represents a callable on an object. @@ -33,6 +32,7 @@ public function getObject(): object /** * @param mixed $args + * * @return mixed */ public function __invoke(...$args) @@ -46,6 +46,6 @@ public function toString(): string { $class = get_class($this->getObject()); - return $class.'::'.$this->callable[1].'()'; + return $class . '::' . $this->callable[1] . '()'; } } diff --git a/src/Middlewares/SourceResolver.php b/src/Middlewares/SourceResolver.php index 2329876200..8f2461aeaf 100644 --- a/src/Middlewares/SourceResolver.php +++ b/src/Middlewares/SourceResolver.php @@ -6,6 +6,7 @@ use TheCodingMachine\GraphQLite\GraphQLRuntimeException; use Webmozart\Assert\Assert; + use function get_class; use function is_object; diff --git a/src/MissingTypeHintRuntimeException.php b/src/MissingTypeHintRuntimeException.php index 2a0a7e7741..01fcbc7529 100644 --- a/src/MissingTypeHintRuntimeException.php +++ b/src/MissingTypeHintRuntimeException.php @@ -6,6 +6,7 @@ use ReflectionMethod; use ReflectionNamedType; + use function assert; use function sprintf; diff --git a/src/NamingStrategy.php b/src/NamingStrategy.php index 1fa2b5a31f..711d26ae91 100644 --- a/src/NamingStrategy.php +++ b/src/NamingStrategy.php @@ -6,6 +6,7 @@ use TheCodingMachine\GraphQLite\Annotations\Factory; use TheCodingMachine\GraphQLite\Annotations\Type; + use function lcfirst; use function str_replace; use function strlen; diff --git a/src/Parameters/MissingArgumentException.php b/src/Parameters/MissingArgumentException.php index 4c288e2abb..1a43e3d893 100644 --- a/src/Parameters/MissingArgumentException.php +++ b/src/Parameters/MissingArgumentException.php @@ -7,6 +7,7 @@ use BadMethodCallException; use TheCodingMachine\GraphQLite\Exceptions\GraphQLExceptionInterface; use TheCodingMachine\GraphQLite\Middlewares\ResolverInterface; + use function get_class; use function is_array; use function is_string; diff --git a/src/QueryField.php b/src/QueryField.php index ca20d0f257..0dd8ea6622 100644 --- a/src/QueryField.php +++ b/src/QueryField.php @@ -23,6 +23,7 @@ use TheCodingMachine\GraphQLite\Parameters\PrefetchDataParameter; use TheCodingMachine\GraphQLite\Parameters\SourceParameter; use Webmozart\Assert\Assert; + use function array_unshift; use function get_class; use function is_object; @@ -222,10 +223,10 @@ public static function fromFieldDescriptor(QueryFieldDescriptor $fieldDescriptor { $arguments = $fieldDescriptor->getParameters(); if ($fieldDescriptor->getPrefetchMethodName() !== null) { - $arguments = [ '__graphqlite_prefectData' => new PrefetchDataParameter() ] + $arguments; + $arguments = ['__graphqlite_prefectData' => new PrefetchDataParameter()] + $arguments; } if ($fieldDescriptor->isInjectSource() === true) { - $arguments = [ '__graphqlite_source' => new SourceParameter() ] + $arguments; + $arguments = ['__graphqlite_source' => new SourceParameter()] + $arguments; } $fieldDescriptor->setParameters($arguments); diff --git a/src/QueryFieldDescriptor.php b/src/QueryFieldDescriptor.php index aaf015c496..df0a324888 100644 --- a/src/QueryFieldDescriptor.php +++ b/src/QueryFieldDescriptor.php @@ -6,7 +6,6 @@ use GraphQL\Type\Definition\OutputType; use GraphQL\Type\Definition\Type; -use InvalidArgumentException; use ReflectionMethod; use TheCodingMachine\GraphQLite\Annotations\MiddlewareAnnotations; use TheCodingMachine\GraphQLite\Middlewares\MagicPropertyResolver; diff --git a/src/Reflection/CachedDocBlockFactory.php b/src/Reflection/CachedDocBlockFactory.php index 0a8b20c429..c729ab18d4 100644 --- a/src/Reflection/CachedDocBlockFactory.php +++ b/src/Reflection/CachedDocBlockFactory.php @@ -12,6 +12,7 @@ use ReflectionClass; use ReflectionMethod; use Webmozart\Assert\Assert; + use function filemtime; use function md5; diff --git a/src/Reflection/ReflectionInterfaceUtils.php b/src/Reflection/ReflectionInterfaceUtils.php index 1c6a0bbf68..aaedbc3d46 100644 --- a/src/Reflection/ReflectionInterfaceUtils.php +++ b/src/Reflection/ReflectionInterfaceUtils.php @@ -5,6 +5,7 @@ namespace TheCodingMachine\GraphQLite\Reflection; use ReflectionClass; + use function array_diff_key; class ReflectionInterfaceUtils diff --git a/src/ResolveUtils.php b/src/ResolveUtils.php index 85f0dcc2f1..714db16671 100644 --- a/src/ResolveUtils.php +++ b/src/ResolveUtils.php @@ -8,6 +8,7 @@ use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; + use function is_array; use function is_iterable; use function is_object; diff --git a/src/SchemaFactory.php b/src/SchemaFactory.php index a697fe9fd7..89ddca0714 100644 --- a/src/SchemaFactory.php +++ b/src/SchemaFactory.php @@ -51,6 +51,7 @@ use TheCodingMachine\GraphQLite\Types\TypeResolver; use TheCodingMachine\GraphQLite\Utils\NamespacedCache; use TheCodingMachine\GraphQLite\Utils\Namespaces\NamespaceFactory; + use function array_map; use function array_reverse; use function class_exists; diff --git a/src/Security/SecurityExpressionLanguageProvider.php b/src/Security/SecurityExpressionLanguageProvider.php index 14a528fa50..ae3cc8dcc6 100644 --- a/src/Security/SecurityExpressionLanguageProvider.php +++ b/src/Security/SecurityExpressionLanguageProvider.php @@ -6,6 +6,7 @@ use Symfony\Component\ExpressionLanguage\ExpressionFunction; use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; + use function sprintf; /** diff --git a/src/TypeGenerator.php b/src/TypeGenerator.php index 6529721698..d9c85f1712 100644 --- a/src/TypeGenerator.php +++ b/src/TypeGenerator.php @@ -13,6 +13,7 @@ use TheCodingMachine\GraphQLite\Types\MutableObjectType; use TheCodingMachine\GraphQLite\Types\TypeAnnotatedInterfaceType; use TheCodingMachine\GraphQLite\Types\TypeAnnotatedObjectType; + use function interface_exists; /** diff --git a/src/TypeRegistry.php b/src/TypeRegistry.php index 21da7734b2..f7205edfbe 100644 --- a/src/TypeRegistry.php +++ b/src/TypeRegistry.php @@ -14,6 +14,7 @@ use TheCodingMachine\GraphQLite\Types\MutableObjectType; use TheCodingMachine\GraphQLite\Types\ResolvableMutableInputInterface; use TheCodingMachine\GraphQLite\Types\UnionType; + use function get_class; /** diff --git a/src/Types/ArgumentResolver.php b/src/Types/ArgumentResolver.php index ad9766c798..3a5b972c2e 100644 --- a/src/Types/ArgumentResolver.php +++ b/src/Types/ArgumentResolver.php @@ -16,6 +16,7 @@ use InvalidArgumentException; use RuntimeException; use Webmozart\Assert\Assert; + use function array_map; use function get_class; use function is_array; diff --git a/src/Types/ID.php b/src/Types/ID.php index 88b2331a4b..9da38a9adc 100644 --- a/src/Types/ID.php +++ b/src/Types/ID.php @@ -5,6 +5,7 @@ namespace TheCodingMachine\GraphQLite\Types; use InvalidArgumentException; + use function is_object; use function is_scalar; use function method_exists; diff --git a/src/Types/InterfaceFromObjectType.php b/src/Types/InterfaceFromObjectType.php index 7c23781f81..07227a30ef 100644 --- a/src/Types/InterfaceFromObjectType.php +++ b/src/Types/InterfaceFromObjectType.php @@ -10,6 +10,7 @@ use GraphQL\Type\Definition\Type; use InvalidArgumentException; use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface; + use function get_class; use function gettype; use function is_object; diff --git a/src/Types/MutableInterfaceType.php b/src/Types/MutableInterfaceType.php index 45731d9334..4c69496e23 100644 --- a/src/Types/MutableInterfaceType.php +++ b/src/Types/MutableInterfaceType.php @@ -15,6 +15,7 @@ class MutableInterfaceType extends InterfaceType implements MutableInterface /** * @param mixed[] $config + * @param class-string|null $className */ public function __construct(array $config, ?string $className = null) { diff --git a/src/Types/MutableObjectType.php b/src/Types/MutableObjectType.php index e9aa0f1173..0a49b1e44e 100644 --- a/src/Types/MutableObjectType.php +++ b/src/Types/MutableObjectType.php @@ -15,6 +15,7 @@ class MutableObjectType extends ObjectType implements MutableInterface /** * @param mixed[] $config + * @param class-string|null $className */ public function __construct(array $config, ?string $className = null) { diff --git a/src/Types/MutableTrait.php b/src/Types/MutableTrait.php index 91f6a9d944..d1d6537bfa 100644 --- a/src/Types/MutableTrait.php +++ b/src/Types/MutableTrait.php @@ -19,7 +19,7 @@ trait MutableTrait /** @var FieldDefinition[]|null */ private $finalFields; - /** @var string|null */ + /** @var class-string|null */ private $className; public function freeze(): void @@ -96,6 +96,8 @@ public function getFields(): array /** * Returns the PHP class mapping this GraphQL type (if any) + * + * @return class-string|null */ public function getMappedClassName(): ?string { diff --git a/src/Types/ResolvableMutableInputObjectType.php b/src/Types/ResolvableMutableInputObjectType.php index 6bb1ab5bf9..aefa850890 100644 --- a/src/Types/ResolvableMutableInputObjectType.php +++ b/src/Types/ResolvableMutableInputObjectType.php @@ -14,6 +14,7 @@ use TheCodingMachine\GraphQLite\Parameters\MissingArgumentException; use TheCodingMachine\GraphQLite\Parameters\ParameterInterface; use Webmozart\Assert\Assert; + use function count; use function is_array; @@ -50,7 +51,7 @@ class ResolvableMutableInputObjectType extends MutableInputObjectType implements */ public function __construct(string $name, FieldsBuilder $fieldsBuilder, $factory, string $methodName, ?string $comment, bool $canBeInstantiatedWithoutParameters, array $additionalConfig = []) { - $resolve = [ $factory, $methodName ]; + $resolve = [$factory, $methodName]; Assert::isCallable($resolve); $this->resolve = $resolve; $this->fieldsBuilder = $fieldsBuilder; @@ -126,7 +127,7 @@ public function resolve(?object $source, array $args, $context, ResolveInfo $res foreach ($this->decorators as $key => $decorator) { $decoratorParameters = $this->getParametersForDecorator($key); - $toPassArgs = [ $object ]; + $toPassArgs = [$object]; foreach ($decoratorParameters as $parameter) { try { $toPassArgs[] = $parameter->resolve($source, $args, $context, $resolveInfo); @@ -151,7 +152,7 @@ public function decorate(callable $decorator): void { $this->decorators[] = $decorator; - $key = count($this->decorators)-1; + $key = count($this->decorators) - 1; $this->addFields(function () use ($key) { return InputTypeUtils::getInputTypeArgs($this->getParametersForDecorator($key)); diff --git a/src/Types/TypeAnnotatedInterfaceType.php b/src/Types/TypeAnnotatedInterfaceType.php index 0f46d806da..835fb46e23 100644 --- a/src/Types/TypeAnnotatedInterfaceType.php +++ b/src/Types/TypeAnnotatedInterfaceType.php @@ -26,7 +26,8 @@ class TypeAnnotatedInterfaceType extends MutableInterfaceType private $recursiveTypeMapper; /** - * @param mixed[] $config + * @param class-string $className + * @param RecursiveTypeMapperInterface $recursiveTypeMapper */ public function __construct(string $className, array $config, RecursiveTypeMapperInterface $recursiveTypeMapper) { diff --git a/src/Types/TypeAnnotatedObjectType.php b/src/Types/TypeAnnotatedObjectType.php index f0b6a894eb..791004f8c3 100644 --- a/src/Types/TypeAnnotatedObjectType.php +++ b/src/Types/TypeAnnotatedObjectType.php @@ -6,6 +6,7 @@ use TheCodingMachine\GraphQLite\FieldsBuilder; use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface; + use function class_implements; use function get_parent_class; @@ -15,6 +16,7 @@ class TypeAnnotatedObjectType extends MutableObjectType { /** + * @param class-string $className * @param mixed[] $config */ public function __construct(string $className, array $config) diff --git a/src/Types/UnionType.php b/src/Types/UnionType.php index e914663625..b3ba5e3554 100644 --- a/src/Types/UnionType.php +++ b/src/Types/UnionType.php @@ -7,6 +7,7 @@ use GraphQL\Type\Definition\ObjectType; use InvalidArgumentException; use TheCodingMachine\GraphQLite\Mappers\RecursiveTypeMapperInterface; + use function get_class; use function gettype; use function is_object; diff --git a/src/Utils/Namespaces/NS.php b/src/Utils/Namespaces/NS.php index dde5adfc63..d991ddc95e 100644 --- a/src/Utils/Namespaces/NS.php +++ b/src/Utils/Namespaces/NS.php @@ -8,6 +8,7 @@ use Psr\SimpleCache\CacheInterface; use ReflectionClass; use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer; + use function class_exists; use function interface_exists; diff --git a/tests/AnnotationReaderTest.php b/tests/AnnotationReaderTest.php index b7c99f5f92..bb57bd6714 100644 --- a/tests/AnnotationReaderTest.php +++ b/tests/AnnotationReaderTest.php @@ -2,6 +2,7 @@ namespace TheCodingMachine\GraphQLite; +use TheCodingMachine\GraphQLite\Annotations\Autowire; use Doctrine\Common\Annotations\AnnotationException; use InvalidArgumentException; use PHPUnit\Framework\TestCase; @@ -10,10 +11,12 @@ use ReflectionMethod; use TheCodingMachine\GraphQLite\Annotations\Exceptions\ClassNotFoundException; use TheCodingMachine\GraphQLite\Annotations\Field; +use TheCodingMachine\GraphQLite\Annotations\Security; use TheCodingMachine\GraphQLite\Annotations\Type; use TheCodingMachine\GraphQLite\Fixtures\Annotations\ClassWithInvalidClassAnnotation; use TheCodingMachine\GraphQLite\Fixtures\Annotations\ClassWithInvalidExtendTypeAnnotation; use TheCodingMachine\GraphQLite\Fixtures\Annotations\ClassWithInvalidTypeAnnotation; +use TheCodingMachine\GraphQLite\Fixtures\Attributes\TestType; class AnnotationReaderTest extends TestCase { @@ -150,4 +153,77 @@ public function testEmptyGetParameterAnnotations(): void $this->assertEmpty($annotationReader->getParameterAnnotationsPerParameter([])); } + + /** + * @requires PHP >= 8.0 + */ + public function testPhp8AttributeClassAnnotation(): void + { + $annotationReader = new AnnotationReader(new DoctrineAnnotationReader()); + + $type = $annotationReader->getTypeAnnotation(new ReflectionClass(TestType::class)); + $this->assertSame(TestType::class, $type->getClass()); + + // We get the same instance + //$type2 = $annotationReader->getTypeAnnotation(new ReflectionClass(TestType::class)); + //$this->assertSame($type, $type2, 'Assert some cache is available'); + } + + /** + * @requires PHP >= 8.0 + */ + public function testPhp8AttributeClassAnnotations(): void + { + $annotationReader = new AnnotationReader(new DoctrineAnnotationReader()); + + $types = $annotationReader->getSourceFields(new ReflectionClass(TestType::class)); + + $this->assertCount(3, $types); + } + + /** + * @requires PHP >= 8.0 + */ + public function testPhp8AttributeMethodAnnotation(): void + { + $annotationReader = new AnnotationReader(new DoctrineAnnotationReader()); + + $type = $annotationReader->getRequestAnnotation(new ReflectionMethod(TestType::class, 'getField'), Field::class); + $this->assertInstanceOf(Field::class, $type); + } + + /** + * @requires PHP >= 8.0 + */ + public function testPhp8AttributeMethodAnnotations(): void + { + $annotationReader = new AnnotationReader(new DoctrineAnnotationReader()); + + $middlewareAnnotations = $annotationReader->getMiddlewareAnnotations(new ReflectionMethod(TestType::class, 'getField')); + + /** @var Security[] $securitys */ + $securitys = $middlewareAnnotations->getAnnotationsByType(Security::class); + $this->assertCount(2, $securitys); + $this->assertFalse($securitys[0]->isFailWithSet()); + $this->assertNull($securitys[1]->getFailWith()); + $this->assertTrue($securitys[1]->isFailWithSet()); + } + + /** + * @requires PHP >= 8.0 + */ + public function testPhp8AttributeParameterAnnotations(): void + { + $annotationReader = new AnnotationReader(new DoctrineAnnotationReader()); + + $parameterAnnotations = $annotationReader->getParameterAnnotationsPerParameter((new ReflectionMethod(__CLASS__, 'method1'))->getParameters()); + + $this->assertInstanceOf(Autowire::class, $parameterAnnotations['dao']->getAnnotationByType(Autowire::class)); + } + + private function method1( + #[Autowire('myService')] + $dao + ): void { + } } diff --git a/tests/Annotations/AutowireTest.php b/tests/Annotations/AutowireTest.php index 04a6d201a5..acc8883e4f 100644 --- a/tests/Annotations/AutowireTest.php +++ b/tests/Annotations/AutowireTest.php @@ -12,6 +12,6 @@ public function testException(): void { $this->expectException(BadMethodCallException::class); $this->expectExceptionMessage('The @Autowire annotation must be passed a target. For instance: "@Autowire(for="$myService")"'); - new Autowire([]); + (new Autowire([]))->getTarget(); } } diff --git a/tests/Annotations/DecorateTest.php b/tests/Annotations/DecorateTest.php index bd7f902f55..16f82d5e53 100644 --- a/tests/Annotations/DecorateTest.php +++ b/tests/Annotations/DecorateTest.php @@ -4,6 +4,7 @@ use BadMethodCallException; use PHPUnit\Framework\TestCase; +use ReflectionMethod; class DecorateTest extends TestCase { @@ -14,4 +15,18 @@ public function testException(): void $this->expectExceptionMessage('The @Decorate annotation must be passed an input type. For instance: "@Decorate("MyInputType")"'); new Decorate([]); } + + /** + * @requires PHP >= 8.0 + */ + public function testPhp8Annotation(): void + { + $method = new ReflectionMethod(__CLASS__, 'method1'); + $attribute = $method->getAttributes()[0]->newInstance(); + $this->assertSame('foobar', $attribute->getInputTypeName()); + } + + #[Decorate("foobar")] + public function method1(): void { + } } diff --git a/tests/Annotations/FailWithTest.php b/tests/Annotations/FailWithTest.php index 8b0964381c..720d24ae3e 100644 --- a/tests/Annotations/FailWithTest.php +++ b/tests/Annotations/FailWithTest.php @@ -4,6 +4,7 @@ use BadMethodCallException; use PHPUnit\Framework\TestCase; +use ReflectionMethod; class FailWithTest extends TestCase { @@ -14,4 +15,19 @@ public function testException(): void $this->expectExceptionMessage('The @FailWith annotation must be passed a defaultValue. For instance: "@FailWith(null)"'); new FailWith([]); } + + /** + * @requires PHP >= 8.0 + */ + public function testPhp8Annotation(): void + { + $method = new ReflectionMethod(__CLASS__, 'method1'); + $failWith = $method->getAttributes()[0]->newInstance(); + $this->assertSame(null, $failWith->getValue()); + } + + #[FailWith(value: null)] + public function method1(): void { + } + } diff --git a/tests/Annotations/HideParameterTest.php b/tests/Annotations/HideParameterTest.php index 449a1c39d3..e4856d4c07 100644 --- a/tests/Annotations/HideParameterTest.php +++ b/tests/Annotations/HideParameterTest.php @@ -10,6 +10,6 @@ class HideParameterTest extends TestCase public function testException(): void { $this->expectException(BadMethodCallException::class); - new HideParameter([]); + (new HideParameter([]))->getTarget(); } } diff --git a/tests/Annotations/InjectUserTest.php b/tests/Annotations/InjectUserTest.php index ac9b844158..a33956538d 100644 --- a/tests/Annotations/InjectUserTest.php +++ b/tests/Annotations/InjectUserTest.php @@ -10,6 +10,6 @@ class InjectUserTest extends TestCase public function testException(): void { $this->expectException(BadMethodCallException::class); - new InjectUser([]); + (new InjectUser([]))->getTarget(); } } diff --git a/tests/Annotations/RightTest.php b/tests/Annotations/RightTest.php index f2c34c10e6..224aa65a31 100644 --- a/tests/Annotations/RightTest.php +++ b/tests/Annotations/RightTest.php @@ -2,6 +2,7 @@ namespace TheCodingMachine\GraphQLite\Annotations; +use \ReflectionMethod; use BadMethodCallException; use PHPUnit\Framework\TestCase; @@ -14,4 +15,26 @@ public function testException(): void $this->expectExceptionMessage('The @Right annotation must be passed a right name. For instance: "@Right(\'my_right\')"'); new Right([]); } + + /** + * @requires PHP >= 8.0 + */ + public function testPhp8Annotation(): void + { + $method = new ReflectionMethod(__CLASS__, 'method1'); + $right = $method->getAttributes()[0]->newInstance(); + $this->assertSame('foo', $right->getName()); + + $method = new ReflectionMethod(__CLASS__, 'method2'); + $right = $method->getAttributes()[0]->newInstance(); + $this->assertSame('foo', $right->getName()); + } + + #[Right(name: "foo")] + public function method1(): void { + } + + #[Right("foo")] + public function method2(): void { + } } diff --git a/tests/Annotations/SecurityTest.php b/tests/Annotations/SecurityTest.php index b92d858797..c7fcf22521 100644 --- a/tests/Annotations/SecurityTest.php +++ b/tests/Annotations/SecurityTest.php @@ -4,6 +4,8 @@ use BadMethodCallException; use PHPUnit\Framework\TestCase; +use stdClass; +use TypeError; class SecurityTest extends TestCase { @@ -21,4 +23,11 @@ public function testIncompatibleParams(): void $this->expectExceptionMessage('A @Security annotation that has "failWith" attribute set cannot have a message or a statusCode attribute.'); new Security(['expression'=>'foo', 'failWith'=>null, 'statusCode'=>500]); } + + public function testBadParams2(): void + { + $this->expectException(TypeError::class); + $this->expectExceptionMessage('"TheCodingMachine\GraphQLite\Annotations\Security::__construct": Argument $data is expected to be a string or array, got "object".'); + new Security(new stdClass()); + } } diff --git a/tests/Annotations/UseInputTypeTest.php b/tests/Annotations/UseInputTypeTest.php index e683de06b1..2e948a9e22 100644 --- a/tests/Annotations/UseInputTypeTest.php +++ b/tests/Annotations/UseInputTypeTest.php @@ -4,6 +4,7 @@ use BadMethodCallException; use PHPUnit\Framework\TestCase; +use ReflectionMethod; class UseInputTypeTest extends TestCase { @@ -11,7 +12,23 @@ class UseInputTypeTest extends TestCase public function testException(): void { $this->expectException(BadMethodCallException::class); - $this->expectExceptionMessage('The @UseInputType annotation must be passed a target and an input type. For instance: "@UseInputType(for="$input", inputType="MyInputType")"'); + $this->expectExceptionMessage('The @UseInputType annotation must be passed an input type. For instance: "@UseInputType(for="$input", inputType="MyInputType")" in PHP 7+ or #[UseInputType("MyInputType")] in PHP 8+'); new UseInputType([]); } + + public function testException2(): void + { + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('The @UseInputType annotation must be passed a target and an input type. For instance: "@UseInputType(for="$input", inputType="MyInputType")" in PHP 7+ or #[UseInputType("MyInputType")] in PHP 8+'); + (new UseInputType(['inputType' => 'foo']))->getTarget(); + } + + /** + * @requires PHP >= 8.0 + */ + public function testPhp8Annotation(): void + { + $attribute = new UseInputType('foo'); + $this->assertSame('foo', $attribute->getInputType()); + } } diff --git a/tests/Fixtures/Attributes/TestType.php b/tests/Fixtures/Attributes/TestType.php new file mode 100644 index 0000000000..6dafcdbde9 --- /dev/null +++ b/tests/Fixtures/Attributes/TestType.php @@ -0,0 +1,28 @@ +addParamInfo((new ReflectionClass('DateTime'))->getMethod('__construct')->getParameters()[0]); @@ -19,6 +21,18 @@ public function testAddParamInfo() $this->assertSame('For parameter $time, in DateTime::__construct, cannot map class "Foo" to a known GraphQL type. Check your TypeMapper configuration.', $e->getMessage()); } + /** + * @requires PHP >= 8.0 + */ + public function testAddParamInfoSupphp74() + { + $e = CannotMapTypeException::createForType('Foo'); + $e->addParamInfo((new ReflectionClass('DateTime'))->getMethod('__construct')->getParameters()[0]); + $e->addParamInfo((new ReflectionClass('DateTime'))->getMethod('__construct')->getParameters()[0]); + + $this->assertSame('For parameter $datetime, in DateTime::__construct, cannot map class "Foo" to a known GraphQL type. Check your TypeMapper configuration.', $e->getMessage()); + } + public function testAddSourceFieldInfo() { $class = new ReflectionClass(TestTypeId::class); diff --git a/website/sidebars.json b/website/sidebars.json index ee1f2549d9..a8c95f9af0 100755 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -6,6 +6,6 @@ "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": ["annotations_reference", "semver","changelog"] + "Reference": ["doctrine-annotations-attributes", "annotations_reference", "semver","changelog"] } } diff --git a/website/versioned_docs/version-4.0/annotations_reference.md b/website/versioned_docs/version-4.0/annotations_reference.md index 8e8e57dbae..c89d5f29da 100644 --- a/website/versioned_docs/version-4.0/annotations_reference.md +++ b/website/versioned_docs/version-4.0/annotations_reference.md @@ -25,7 +25,7 @@ The `@Mutation` annotation is used to declare a GraphQL mutation. Attribute | Compulsory | Type | Definition ---------------|------------|------|-------- name | *no* | string | The name of the mutation. If skipped, the name of the method is used instead. -[outputType](custom_output_types.md) | *no* | string | Forces the GraphQL output type of a query. +[outputType](custom_types.md) | *no* | string | Forces the GraphQL output type of a query. ## @Type annotation