Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"thecodingmachine/class-explorer": "^1.0.2",
"psr/simple-cache": "^1",
"phpdocumentor/reflection-docblock": "^4.3",
"phpdocumentor/type-resolver": "^0.4 || ^1.0",
"phpdocumentor/type-resolver": "^1.0.1",
"psr/http-message": "^1",
"ecodev/graphql-upload": "^4.0",
"webmozart/assert": "^1.4",
Expand Down
56 changes: 56 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
id: changelog
title: Changelog
sidebar_label: Changelog
---

## 4.0-beta1

This is a complete refactoring from 3.x. While existing annotations are kept compatible, the internals have completely
changed.

New features:

- You can directly [annotate a PHP interface with `@Type` to make it a GraphQL interface](inheritance-interfaces.md#mapping-interfaces)
- You can autowire services in resolvers, thanks to the new `@Autowire` annotation
- Added [user input validation](validation.md) (using the Symfony Validator or the Laravel validator or a custom `@Assertion` annotation
- Improved security handling:
- Unauthorized access to fields can now generate GraphQL errors (rather that schema errors in GraphQLite v3)
- Added fine-grained security using the `@Security` annotation. A field can now be [marked accessible or not depending on the context](fine-grained-security.md).
For instance, you can restrict access to the field "viewsCount" of the type `BlogPost` only for post that the current user wrote.
- Performance:
- You can inject the [Webonyx query plan in a parameter from a resolver](query_plan.md)
- You can use the [dataloader pattern to improve performance drastically via the "prefetchMethod" attribute](prefetch_method.md)
- Customizable error handling has been added:
- You can throw [GraphQL errors](error_handling.md) with `TheCodingMachine\GraphQLite\Exceptions\GraphQLException`
- You can specify the HTTP response code to send with a given error, and the errors "extensions" section
- You can throw [many errors in one exception](error_handling.md#many-errors-for-one-exception) with `TheCodingMachine\GraphQLite\Exceptions\GraphQLAggregateException`
- You can map [a given PHP class to several PHP input types](input_types.md#declaring-several-input-types-for-the-same-php-class) (a PHP class can have several `@Factory` annotations)
- You can force input types using `@UseInputType(for="$id", inputType="ID!")`
- You can extend an input types (just like you could extend an output type in v3) using [the new `@Decorate` annotation](extend_input_type.md)
- In a factory, you can [exclude some optional parameters from the GraphQL schema](input_types#ignoring-some-parameters)


Many extension points have been added

- Added a "root type mapper" (useful to map scalar types to PHP types or to add custom annotations related to resolvers)
- Added ["field middlewares"](field_middlewares.md) (useful to add middleware that modify the way GraphQL fields are handled)
- Added a ["parameter type mapper"](argument_resolving.md) (useful to add customize parameter resolution or add custom annotations related to parameters)

New framework specific features:

Symfony:

- The Symfony bundle now provides a "login" and a "logout" mutation (and also a "me" query)

Laravel:

- [Native integration with the Laravel paginator](laravel-package-advanced.md#support-for-pagination) has been added

Internals:

- The `FieldsBuilder` class has been split in many different services (`FieldsBuilder`, `TypeHandler`, and a
chain of *root type mappers*)
- The `FieldsBuilderFactory` class has been completely removed.
- Overall, there is not much in common internally between 4.x and 3.x. 4.x is much more flexible with many more hook points
than 3.x. Try it out!
47 changes: 42 additions & 5 deletions docs/custom_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,16 @@ In order to add a scalar type in GraphQLite, you need to:
- create a [Webonyx custom scalar type](https://webonyx.github.io/graphql-php/type-system/scalar-types/#writing-custom-scalar-types).
You do this by creating a class that extends `GraphQL\Type\Definition\ScalarType`.
- create a "type mapper" that will map PHP types to the GraphQL scalar type. You do this by writing a class implementing the `RootTypeMapperInterface`.
- create a "type mapper factory" that will be in charge of creating your "type mapper".

```php
interface RootTypeMapperInterface
{
public function toGraphQLOutputType(Type $type, ?OutputType $subType, ReflectionMethod $refMethod, DocBlock $docBlockObj): ?OutputType;
public function toGraphQLOutputType(Type $type, ?OutputType $subType, ReflectionMethod $refMethod, DocBlock $docBlockObj): OutputType;

public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, ReflectionMethod $refMethod, DocBlock $docBlockObj): ?InputType;
public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, ReflectionMethod $refMethod, DocBlock $docBlockObj): InputType;

public function mapNameToType(string $typeName): ?NamedType;
public function mapNameToType(string $typeName): NamedType;
}
```

Expand All @@ -138,19 +139,30 @@ to your GraphQL scalar type. Return your scalar type if there is a match or `nul

The `mapNameToType` should return your GraphQL scalar type if `$typeName` is the name of your scalar type.

RootTypeMapper are organized **in a chain** (they are actually middlewares).
Each instance of a `RootTypeMapper` holds a reference on the next root type mapper to be called in the chain.

For instance:

```php
class AnyScalarTypeMapper implements RootTypeMapperInterface
{
/** @var RootTypeMapperInterface */
private $next;

public function __construct(RootTypeMapperInterface $next)
{
$this->next = $next;
}

public function toGraphQLOutputType(Type $type, ?OutputType $subType, ReflectionMethod $refMethod, DocBlock $docBlockObj): ?OutputType
{
if ($type instanceof Scalar) {
// AnyScalarType is a class implementing the Webonyx ScalarType type.
return AnyScalarType::getInstance();
}
return null;
// If the PHPDoc type is not "Scalar", let's pass the control to the next type mapper in the chain
return $this->next->toGraphQLOutputType($type, $subType, $refMethod, $docBlockObj);
}

public function toGraphQLInputType(Type $type, ?InputType $subType, string $argumentName, ReflectionMethod $refMethod, DocBlock $docBlockObj): ?InputType
Expand All @@ -159,7 +171,8 @@ class AnyScalarTypeMapper implements RootTypeMapperInterface
// AnyScalarType is a class implementing the Webonyx ScalarType type.
return AnyScalarType::getInstance();
}
return null;
// If the PHPDoc type is not "Scalar", let's pass the control to the next type mapper in the chain
return $this->next->toGraphQLInputType($type, $subType, $argumentName, $refMethod, $docBlockObj);
}

/**
Expand All @@ -179,3 +192,27 @@ class AnyScalarTypeMapper implements RootTypeMapperInterface
}
}
```

Now, in order to create an instance of your `AnyScalarTypeMapper` class, you need an instance of the `$next` type mapper in the chain.
How do you get the `$next` type mapper? Through a factory:

```php
class AnyScalarTypeMapperFactory implements RootTypeMapperFactoryInterface
{
public function create(RootTypeMapperInterface $next, RootTypeMapperFactoryContext $context): RootTypeMapperInterface
{
return new AnyScalarTypeMapper($next);
}
}
```

Now, you need to register this factory in your application, and we are done.

You can register your own root mapper factories using the `SchemaFactory::addRootTypeMapperFactory()` method.

```php
$schemaFactory->addRootTypeMapperFactory(new AnyScalarTypeMapperFactory());
```

If you are using the Symfony bundle, the factory will be automatically registered, you have nothing to do (the service
is automatically tagged with the "graphql.root_type_mapper_factory" tag).
30 changes: 28 additions & 2 deletions docs/internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ mermaid.initialize({
graph TD
classDef custom fill:#cfc,stroke:#7a7,stroke-width:2px,stroke-dasharray: 5, 5;
subgraph RootTypeMapperInterface
NullableTypeMapperAdapter-->CompoundTypeMapper
CompoundTypeMapper-->IteratorTypeMapper
IteratorTypeMapper-->YourCustomRootTypeMapper
YourCustomRootTypeMapper-->MyCLabsEnumTypeMapper
MyCLabsEnumTypeMapper-->BaseTypeMapper
BaseTypeMapper-->FinalRootTypeMapper
end
subgraph RecursiveTypeMapperInterface
BaseTypeMapper-->RecursiveTypeMapper
Expand All @@ -56,17 +60,39 @@ These type mappers are the first type mappers called.
They are responsible for:

- mapping scalar types (for instance mapping the "int" PHP type to GraphQL Integer type)
- detecting nullable/non-nullable types (for instance interpreting "?int" or "int|null")
- mapping list types (mapping a PHP array to a GraphQL list)
- mapping union types
- mapping enums

Root type mappers have access to the *context* of a type: they can access the PHP DocBlock and read annotations.
If you want to write a custom type mapper that needs access to annotations, it needs to be a "root type mapper".

GraphQLite provide 3 default implementations:
GraphQLite provides 6 classes implementing `RootTypeMapperInterface`:

- `CompositeRootTypeMapper`: a type mapper that delegates mapping to other type mappers using the Composite Design Pattern.
- `NullableTypeMapperAdapter`: a type mapper in charge of making GraphQL types non-nullable if the PHP type is non-nullable
- `CompoundTypeMapper`: a type mapper in charge of union types
- `IteratorTypeMapper`: a type mapper in charge of iterable types (for instance: `MyIterator|User[]`)
- `MyCLabsEnumTypeMapper`: maps MyCLabs/enum types to GraphQL enum types
- `BaseTypeMapper`: maps scalar types and lists. Passes the control to the "recursive type mappers" if an object is encountered.
- `FinalRootTypeMapper`: the last type mapper of the chain, used to throw error if no other type mapper managed to handle the type.

Type mappers are organized in a chain; each type-mapper is responsible for calling the next type mapper.

<div class="mermaid">
graph TD
classDef custom fill:#cfc,stroke:#7a7,stroke-width:2px,stroke-dasharray: 5, 5;
subgraph RootTypeMapperInterface
NullableTypeMapperAdapter-->CompoundTypeMapper
CompoundTypeMapper-->IteratorTypeMapper
IteratorTypeMapper-->YourCustomRootTypeMapper
YourCustomRootTypeMapper-->MyCLabsEnumTypeMapper
MyCLabsEnumTypeMapper-->BaseTypeMapper
BaseTypeMapper-->FinalRootTypeMapper
end
class YourCustomRootTypeMapper custom;
</div>


## Class type mappers

Expand Down
12 changes: 10 additions & 2 deletions docs/migrating.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ If you are a "regular" GraphQLite user, migration to v4 should be straightforwar
- Annotations are mostly untouched. The only annotation that is changed is the `@SourceField` annotation.
- Check your code for every places where you use the `@SourceField` annotation:
- The "id" attribute has been remove (`@SourceField(id=true)`). Instead, use `@SourceField(outputType="ID")`
- The "logged", "right" and "failWith" attributes have been remove (`@SourceField(logged=true)`).
- The "logged", "right" and "failWith" attributes have been removed (`@SourceField(logged=true)`).
Instead, use the annotations attribute with the same annotations you use for the `@Field` annotation:
`@SourceField(annotations={@Logged, @FailWith(null)})`
- TODO: change in visibility, new @HideIfUnauthorized
- In GraphQLite v3, the default was to hide a field from the schema if a user has no access to it.
In GraphQLite v4, the default is to still show this field, but to throw an error if the user makes a query on it
(this way, the schema is the same for all users). If you want the old mode, use the new
[`@HideIfUnauthorized` annotation](annotations_reference.md#hideifunauthorized-annotation)
- If you are using the Symfony bundle, the Laravel package or the Universal module, you must also upgrade those to 4.0.
These package will take care of the wiring for you. Apart for upgrading the packages, you have nothing to do.
- If you are relying on the `SchemaFactory` to bootstrap GraphQLite, you have nothing to do.
Expand All @@ -29,3 +32,8 @@ On the other hand, if you are a power user and if you are wiring GraphQLite serv
a look at the `SchemaFactory` class for an example of proper configuration.
- The `HydratorInterface` and all implementations are gone. When returning an input object from a TypeMapper, the object
must now implement the `ResolvableMutableInputInterface` (an input object type that contains its own resolver)

Note: we strongly recommend to use the Symfony bundle, the Laravel package, the Universal module or the SchemaManager
to bootstrap GraphQLite. Wiring directly GraphQLite classes (like the `FieldsBuilder`) into your container is not recommended,
as the signature of the constructor of those classes may vary from one minor release to another.
Use the `SchemaManager` instead.
8 changes: 6 additions & 2 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ parameters:
- "#Property TheCodingMachine\\\\GraphQLite\\\\Types\\\\ResolvableInputObjectType::$resolve \\(array<int, object|string>&callable\\) does not accept array<int,object|string>#"
- "#Variable \\$prefetchRefMethod might not be defined.#"
- "#Parameter \\#2 $type of class TheCodingMachine\\\\GraphQLite\\\\Parameters\\\\InputTypeParameter constructor expects GraphQL\\\\Type\\\\Definition\\\\InputType&GraphQL\\\\Type\\\\Definition\\\\Type, GraphQL\\\\Type\\\\Definition\\\\InputType|GraphQL\\\\Type\\\\Definition\\\\Type given.#"
- "#Parameter \\#1 \\$types of class TheCodingMachine\\\\GraphQLite\\\\Types\\\\UnionType constructor expects array<GraphQL\\\\Type\\\\Definition\\\\ObjectType>, array<int, GraphQL\\\\Type\\\\Definition\\\\Type> given.#"
- "#Access to an undefined property GraphQL\\\\Type\\\\Definition\\\\NamedType::\\$name.#"
- "#Parameter .* of class ReflectionMethod constructor expects string, object|string given.#"
- "#Method TheCodingMachine\\\\GraphQLite\\\\Types\\\\MutableObjectType::getFields() should return array<GraphQL\\\\Type\\\\Definition\\\\FieldDefinition> but returns array|float|int.#"
- "#Parameter \\#2 \\$inputTypeNode of static method GraphQL\\\\Utils\\\\AST::typeFromAST() expects GraphQL\\\\Language\\\\AST\\\\ListTypeNode|GraphQL\\\\Language\\\\AST\\\\NamedTypeNode|GraphQL\\\\Language\\\\AST\\\\NonNullTypeNode, GraphQL\\\\Language\\\\AST\\\\ListTypeNode|GraphQL\\\\Language\\\\AST\\\\NameNode|GraphQL\\\\Language\\\\AST\\\\NonNullTypeNode given.#"
Expand All @@ -14,6 +12,12 @@ parameters:
-
message: '#Parameter .* of class GraphQL\\Error\\Error constructor expects#'
path: src/Exceptions/WebonyxErrorHandler.php
-
message: '#Thrown exceptions in a catch block must bundle the previous exception#'
path: src/Mappers/Root/IteratorTypeMapper.php
-
message: '#Parameter \#2 \$subType of method .* expects#'
path: src/Mappers/Root/IteratorTypeMapper.php
- '#Call to an undefined method GraphQL\\Error\\ClientAware::getMessage()#'
#-
# message: '#If condition is always true#'
Expand Down
2 changes: 1 addition & 1 deletion src/FactoryContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* Those classes are made available to factories implementing QueryProviderFactoryInterface
* or TypeMapperFactoryInterface
*/
class FactoryContext
final class FactoryContext
{
/** @var AnnotationReader */
private $annotationReader;
Expand Down
5 changes: 2 additions & 3 deletions src/FieldsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,14 @@ public function __construct(
NamingStrategyInterface $namingStrategy,
RootTypeMapperInterface $rootTypeMapper,
ParameterMiddlewareInterface $parameterMapper,
FieldMiddlewareInterface $fieldMiddleware,
TypeRegistry $typeRegistry
FieldMiddlewareInterface $fieldMiddleware
) {
$this->annotationReader = $annotationReader;
$this->recursiveTypeMapper = $typeMapper;
$this->typeResolver = $typeResolver;
$this->cachedDocBlockFactory = $cachedDocBlockFactory;
$this->namingStrategy = $namingStrategy;
$this->typeMapper = new TypeHandler($typeMapper, $argumentResolver, $rootTypeMapper, $typeResolver, $typeRegistry);
$this->typeMapper = new TypeHandler($argumentResolver, $rootTypeMapper, $typeResolver);
$this->parameterMapper = $parameterMapper;
$this->fieldMiddleware = $fieldMiddleware;
}
Expand Down
2 changes: 2 additions & 0 deletions src/GlobControllerQueryProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Symfony\Component\Cache\Adapter\Psr16Adapter;
use Symfony\Contracts\Cache\CacheInterface as CacheContractInterface;
use TheCodingMachine\ClassExplorer\Glob\GlobClassExplorer;
use Webmozart\Assert\Assert;
use function class_exists;
use function interface_exists;
use function str_replace;
Expand Down Expand Up @@ -82,6 +83,7 @@ private function getInstancesList(): array
$this->instancesList = $this->cacheContract->get('globQueryProvider', function () {
return $this->buildInstancesList();
});
Assert::isArray($this->instancesList, 'The instance list returned is not an array. There might be an issue with your PSR-16 cache implementation.');
}

return $this->instancesList;
Expand Down
3 changes: 2 additions & 1 deletion src/Mappers/AbstractTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace TheCodingMachine\GraphQLite\Mappers;

use GraphQL\Type\Definition\InputObjectType;
use GraphQL\Type\Definition\NamedType;
use GraphQL\Type\Definition\OutputType;
use GraphQL\Type\Definition\Type;
use Psr\Container\ContainerInterface;
Expand Down Expand Up @@ -308,7 +309,7 @@ public function mapClassToInputType(string $className): ResolvableMutableInputIn
*
* @param string $typeName The name of the GraphQL type
*
* @return Type&((ResolvableMutableInputInterface&InputObjectType)|MutableObjectType|MutableInterfaceType)
* @return NamedType&Type&((ResolvableMutableInputInterface&InputObjectType)|MutableObjectType|MutableInterfaceType)
*
* @throws CannotMapTypeExceptionInterface
* @throws ReflectionException
Expand Down
31 changes: 22 additions & 9 deletions src/Mappers/CannotMapTypeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use TheCodingMachine\GraphQLite\Annotations\ExtendType;
use function array_filter;
use function array_map;
use function implode;
use function sprintf;

class CannotMapTypeException extends Exception implements CannotMapTypeExceptionInterface
{
Expand All @@ -40,21 +40,34 @@ public static function createForParseError(Error $error): self
return new self($error->getMessage(), $error->getCode(), $error);
}

public static function createForMissingIteratorValue(string $className, self $e): self
{
$message = sprintf(
'"%s" is iterable. Please provide a more specific type. For instance: %s|User[].',
$className,
$className
);

return new self($message, 0, $e);
}

/**
* @param Type[] $unionTypes
*
* @return CannotMapTypeException
*/
public static function createForBadTypeInUnion(array $unionTypes): self
{
$disallowedTypes = array_filter($unionTypes, static function (Type $type) {
return $type instanceof NamedType;
});
$disallowedTypeNames = array_map(static function (NamedType $type) {
return $type->name;
}, $disallowedTypes);

return new self('In GraphQL, you can only use union types between objects. These types cannot be used in union types: ' . implode(', ', $disallowedTypeNames));
$disallowedTypeNames = array_map(static function (Type $type) {
return (string) $type;
}, $unionTypes);

return new self('in GraphQL, you can only use union types between objects. These types cannot be used in union types: ' . implode(', ', $disallowedTypeNames));
}

public static function createForBadTypeInUnionWithIterable(Type $type): self
{
return new self('the value must be iterable, but its computed GraphQL type (' . $type . ') is not a list.');
}

public static function mustBeOutputType(string $subTypeName): self
Expand Down
Loading