Download the PHP package le0daniel/graphql-tools without Composer
On this page you can find all versions of the php package le0daniel/graphql-tools. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download le0daniel/graphql-tools
More information about le0daniel/graphql-tools
Files in le0daniel/graphql-tools
Package graphql-tools
Short Description Tools for using endpoints in PHP graphql applications based on webonyx/graphql-php
License MIT
Informations about the package graphql-tools
GraphQl Tools
This is a simple opinionated toolkit for writing scalable code first GraphQL applications in PHP.
Main Features
- Custom extensions support (Tracing, Telemetry of resolvers)
- Support for middlewares of resolvers
- Abstraction for schema registration and multi schema supports. This is similar to the visible function, but more transparent.
- Abstract classes to extend for defining types / enums / interfaces / unions / scalars
- Fields are built with the easy-to-use field builder
- Simple dataloader implementation to solve N+1 problems
- Code First approach to schema building
- Schema Stitching: Extending of Types with additional fields, allowing you to respect your domain boundires and construct the right dependency directions in your code.
- Type name aliases, so that you can either use type names of class names of types.
- Lazy by default for best performance
- Support for @defer for PHP using generators.
Installation
Install via composer
Basic Usage
The usage is similar to how you would use webonyx/graphql
and define object types. Instead of extending the default
classes, you extend the abstract classes provided.
Every resolve function is wrapped by the ProxyResolver class, which provides support for extensions.
At the core, we start with a schema registry. There you register types and register extensions to types.
Schema Registry
The schema registery is the central entity that contains all type definitions for a graphql schema.
You register types, extend types and then create a schema variation. Internally, a TypeRegistry is given to all types and fields, allowing you to seamlessly refer to other types in the schema. The type registry solves the problem that each type class is only instanced once per schema and lazily for best performance.
Loading all types in a directory
To simplify type registration, you can use the TypeMap utility to load all types in a directory. It uses Glob to find all PHP files. In production, you should cache those.
Aliases
In GraphQL, all types have a name. In a code first approach as described here, you define a class, which then creates a name. So in code, you have both the classname and the type name which is used in GraphQL. We automatically create aliases for the classnames. This allows you to reference other types either by their class name or the type name which is used in GraphQL.
Class names can be better to work with inside a module, as you can now statically analyse the class usages.
Creating a schema from the registry
The schema registries task is to create a schema from all registered types dynamically. You can hide and show schema variants by using schema rules. Rules are mostly based on tags. In contrast to using the visibility function, this is more transparent. You can print different Schema variants and use tools to verify the schema or breaking changes. A hidden field can not be queried by any user. This prevents data leakage.
Provided Rules:
- AllVisibleSchemaRule (default): All fields are visible
- TagBasedSchemaRules: Black- and whitelist based on field tags.
You can define your own rules by implementing the SchemaRules interface.
Define Types
In a code first approach to graphql, each type is represented by a class in code.
Naming conventions and classes to extend:
Type | ClassName | Extends | Example |
---|---|---|---|
Object Type | [Name]Type |
GraphQlType |
AnimalType => type Animal |
Input Type | [Name]InputType |
GraphQlInputType |
CreateAnimalInputType => input type CreateAnimalInput |
Interface Type | [Name]Interface |
GraphQlInterface |
MammalInterface => interface Mammal |
Union Type | [Name]Union |
GraphQlUnion |
SearchResultUnion => union SearchResult |
Scalar Type | [Name]Scalar |
GraphQlScalar |
ByteScalar => scalar Byte |
Directive Type | [Name]Directive |
GraphQlDirective |
ExportVariablesDirective => directive ExportVariables |
You can overwrite this behaviour by overwriting the getName function.
Defining Fields and InputFields
In a code first approach, the field definition and the resolve function live in the same place. The field builders allow you to easily construct fields with all required and possible attributes, combined with their resolvers, all in code directly.
Field builders are immutable, and thus flexible to use and reuse.
It attaches a ProxyResolver class to decorate your resolve function under the hood for extensions and Middlewares to work correctly.
To declare types and reference other types, a Type Registry is given to each instance where fields are defined. This allows you to reference other types that exist in your schema. The type registry itself takes care of lazy loading and ensures that every only one instance of a type is created in a schema.
Additionally, you can define Tags, which can be used to define visibility of fields in different schema variations. This is automatically created for you when you create a schema from the schema registry.
By default, the resolver is using the default resolve function from Webonyx (Executor::getDefaultFieldResolver()
).
In its simplest form:
Broader Usage:
Reuse Fields
Creating reusable fields are easy. Create a function or method that returns a field, then it can be used everywhere.
When creating shared fields it is useful to use ofTypeResolver()
. You can define a closure, taking the type registry
as a first argument. This way, you don't need to pass the type registry down.
Cost
In GraphQL, a query can quickly be really expensive to compute, as you can navigate through deep relationships. To prevent users exceeding certain limits, you can limit the complexity a query can have.
Webonyx provides a way to calculate complexity ahead of executing the query. This is done via the MaxComplexityRule. For it to work, each field needs to define a complexity function.
We use the concept of cost, where each field defines its own cost statically and provide a helper to compute variable complexity based on arguments.
In this example we see both components:
- Static costs per field (animals: 2, id: 0, name: 1, relatedAnimals: 2, id: 0, name: 1)
- Variable costs: first: 5
As in the worst case, all 5 entities are loaded from a DataBase, this needs to be taken into account when determining the max cost of the query. Example:
- relatedAnimals cost at max: 5 * (Cost of Animal: 1) + (relatedAnimals price: 2) = 7
- animals cost at max: 5 * (Cost of Animal: 1 + Max(relatedAnimals): 7) + (animals price: 2) = 42
To represent such dynamic costs, you can pass a closure as a second parameter. The return of it will be used to multiply the cost of all children by.
Note: Cost is used to determine the worst case cost of a query. If you want to collect the actual cost, use the ActualCostExtension. It hooks into resolvers and aggregates the cost of fields that were actually present in the query.
Middleware
A middleware is a function that is executed before and after the real resolve function is called. It follows an onion principle. Outer middlewares are called first and invoke the level deeper. You can define multiple middleware functions to prevent data leakage on a complete type or on specific fields.
Middlewares can be defined for a type (applying to all fields) or to a fields. You need to call $next(...)
to invoke
the next layer.
Middlewares allow you to manipulate data that is passed down to the actual field resolver and then the actual result of the resolver. For example, you can use a middleware to validate arguments before the actual resolve function is called.
Signature
Usage You can define multiple middleware for a type (Those middlewares are then pretended to all Fields of that type) or only specific fields.
Extending Types
Extending types and interfaces allows you to add one or many fields to a type outside from its base declaration. This is usually the case when you don't want to cross domain boundaries, but need to add additional fields to another type. This allows you to stitch a schema together.
The simplest way is to use the schema registry and extend an object type or interface, passing a closure that declares additional fields.
Using Classes
Our approach allows to use classes, similar to types, that define type extensions.
ClassName naming patterns for lazy registration to work correctly: Extends[TypeOrInterfaceName](Type|Interface)
Examples:
- ExtendsQueryType => Extends the type with the name Query
- ExtendsUserInterface => Extends the interface with the name User
Federation Middleware
To decouple and remove references to the complete data object in the resolver, a Federation middleware is provided.
2 Middlewares are provided:
Federation::key('id')
, extracts the id property of the data object/arrayFederation::field('id')
, runs the resolver of the field id
Query execution (QueryExecutor)
The query executor is used to execute a query. It attaches Extensions and Validation Rules, handles error mapping and logging.
Extensions: Classes that can drop in to the execution of the query and listen to events. They do not change the result of the query but can collect valuable telemetry data. They are contextual and a new instance is created for each query that is executed. Validation Rules: They validate the query before it is executed. They are not necessarily contextual. If a factory or classname is provided, a new instance is created for each query that is executed. Error Mapper: Receives an instance of Throwable and the corresponding GraphQl Error. It is tasked to map this to a Throwable that potentially implements ClientAware. This allows you to disconnect your internal exceptions from exceptions that you use in GraphQL. Error Logger: Receives an instance of Throwable before it is mapped. This allows you to log errors that occurred.
Extensions and ValidationRules
If you define a factory, it will get the context as argument. This allows you to dynamically create or attach them based
on the user that is executing your query.
If an extension of validation rule implements GraphQlTools\Contract\ProvidesResultExtension
, you can add data to the
extensions array of the result, according to the graphql spec.
Defer (@defer directive)
You can use and enable @defer extension by adding DeferExtension
to your Query executor. It is highly recommended to
also add the ValidateDeferUsageOnFields
validation rule to limit how many defers are allowed per query.
To use it, behind the scenes, the query is run multiple times with cached results. This enables us to defer resolution of some fields to the consequent execution.
ValidationRules
We use the default validation rules from webonyx/graphql with an additional rule to collect deprecation notices.
You can define custom validation rules, by extending the default ValidationRule class from webonyx/graphql. If you additionally implement the ProvidesResultExtension, rules can add an entry to the extensions field in the result. Validation rules are executed before the query is actually run.
Extensions
Extensions are able to hook into the execution and collect data during execution. They allow you to collect traces or telemetry data. Extensions are not allowed to manipulate results of a field resolver. If you want to manipulate results, you need to use middlewares. Extensions are contextual and a new instance is created each time a query is run. You can define extensions by passing a classname to the query executor or a factory. To each factory, the current Context is passed in.
To define an extension, a class needs to implement the ExecutionExtension interface. Extensions can additionally implement ProvidesResultExtension and add entries to the extensions field in the result. The abstract class Extension implements some helper and general logic to build extensions easily.
Following events are provided:
- StartEvent: when the execution is started, but no code has been run yet
- ParsedEvent: once the query is successfully parsed
- EndEvent: once the execution is done
Each event contains specific properties and the time of the event in nanoseconds.
During execution, extensions can hook into the resolve function of each resolver using the visitField hook. You can pass a closure back, which is executed once resolution is done. This is then executed after all promises are resolved, giving you access to the actual data resolved for a field. In case of a failure, a throwable is returned.