- 2023-12-12 - New version 9.0 is released dropping support for PHP 8.0
This library provides a GraphQL driver for Doctrine ORM for use with webonyx/graphql-php. Configuration is available from simple to verbose. Multiple configurations for multiple drivers are supported.
This library does not try to redefine how the excellent library webonyx/graphql-php operates. Instead, it creates types to be used within the framework that library provides.
Please read the detailed documentation.
For an example implementation post to https://graphql.lcdb.org/
For an example application see https://github.com/lcdborg/graphql.lcdb.org
- PHP 8 Attributes for configuration
- Multiple configuration group support
- Supports all Doctrine Types and allows custom types
- Pagination with the GraphQL Complete Connection Model
- Supports filtering of sub-collections
- Events for modifying queries, entity types and more
- Uses the Doctrine Laminas Hydrator for extraction by value or by reference
- Conforms to the Doctrine Coding Standard
Run the following to install this library using Composer:
composer require api-skeletons/doctrine-orm-graphql
Add attributes to your Doctrine entities or see globalEnable for all entities in your schema without attribute configuration.
use ApiSkeletons\Doctrine\ORM\GraphQL\Attribute as GraphQL;
#[GraphQL\Entity]
class Artist
{
#[GraphQL\Field]
public $id;
#[GraphQL\Field]
public $name;
#[GraphQL\Association]
public $performances;
}
#[GraphQL\Entity]
class Performance
{
#[GraphQL\Field]
public $id;
#[GraphQL\Field]
public $venue;
/**
* Not all fields need attributes.
* Only add attribues to fields you want available in GraphQL
*/
public $city;
}
Create the driver and GraphQL schema
use ApiSkeletons\Doctrine\ORM\GraphQL\Driver;
use Doctrine\ORM\EntityManager;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
$driver = new Driver($entityManager);
$schema = new Schema([
'query' => new ObjectType([
'name' => 'query',
'fields' => [
'artists' => [
'type' => $driver->connection($driver->type(Artist::class)),
'args' => [
'filter' => $driver->filter(Artist::class),
'pagination' => $driver->pagination(),
],
'resolve' => $driver->resolve(Artist::class),
],
],
]),
'mutation' => new ObjectType([
'name' => 'mutation',
'fields' => [
'artistUpdateName' => [
'type' => $driver->type(Artist::class),
'args' => [
'id' => Type::nonNull(Type::id()),
'input' => Type::nonNull($driver->input(Artist::class, ['name'])),
],
'resolve' => function ($root, $args) use ($driver): Artist {
$artist = $driver->get(EntityManager::class)
->getRepository(Artist::class)
->find($args['id']);
$artist->setName($args['input']['name']);
$driver->get(EntityManager::class)->flush();
return $artist;
},
],
],
]),
]);
Run GraphQL queries
use GraphQL\GraphQL;
$query = '{
artists {
edges {
node {
id
name
performances {
edges {
node {
venue
}
}
}
}
}
}
}';
$result = GraphQL::executeQuery(
schema: $schema,
source: $query,
variableValues: null,
operationName: null
);
$output = $result->toArray();
Run GraphQL mutations
use GraphQL\GraphQL;
$query = '
mutation ArtistUpdateName($id: Int!, $name: String!) {
artistUpdateName(id: $id, input: { name: $name }) {
id
name
}
}
';
$result = GraphQL::executeQuery(
schema: $schema,
source: $query,
variableValues: [
'id' => 1,
'name' => 'newName',
],
operationName: 'ArtistUpdateName'
);
$output = $result->toArray();
For every enabled field and association, filters are available for querying.
Example
{
artists (
filter: {
name: {
contains: "dead"
}
}
) {
edges {
node {
id
name
performances (
filter: {
venue: {
eq: "The Fillmore"
}
}
) {
edges {
node {
venue
}
}
}
}
}
}
}
Each field has their own set of filters. Most fields have the following:
- eq - Equals.
- neq - Not equals.
- lt - Less than.
- lte - Less than or equal to.
- gt - Greater than.
- gte - Greater than or equal to.
- isnull - Is null. If value is true, the field must be null. If value is false, the field must not be null.
- between - Between. Identical to using gte & lte on the same field. Give values as
low, high
. - in - Exists within an array.
- notin - Does not exist within an array.
- startwith - A like query with a wildcard on the right side of the value.
- endswith - A like query with a wildcard on the left side of the value.
- contains - A like query.
You may exclude any filter from any entity, association, or globally.
You may modify the query builder used to resolve any connection by subscribing to events.
Each connection may have a unique event name. Entity::class . '.filterQueryBuilder'
is recommended.
Pass as the second parameter to $driver->resolve()
.
use ApiSkeletons\Doctrine\ORM\GraphQL\Event\QueryBuilder;
use App\ORM\Entity\Artist;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Schema;
use League\Event\EventDispatcher;
$schema = new Schema([
'query' => new ObjectType([
'name' => 'query',
'fields' => [
'artists' => [
'type' => $driver->connection($driver->type(Artist::class)),
'args' => [
'filter' => $driver->filter(Artist::class),
'pagination' => $driver->pagination(),
],
'resolve' => $driver->resolve(Artist::class, Artist::class . '.queryBuilder'),
],
],
]),
]);
$driver->get(EventDispatcher::class)->subscribeTo(Artist::class . '.queryBuilder',
function(QueryBuilder $event) {
$event->getQueryBuilder()
->innerJoin('entity.user', 'user')
->andWhere($event->getQueryBuilder()->expr()->eq('user.id', ':userId'))
->setParameter('userId', currentUser()->getId())
;
}
);
You may modify the criteria object used to filter associations. For instance, if you use soft deletes then you would want to filter out deleted rows from an association.
use ApiSkeletons\Doctrine\ORM\GraphQL\Attribute as GraphQL;
use ApiSkeletons\Doctrine\ORM\GraphQL\Event\Criteria;
use App\ORM\Entity\Artist;
use League\Event\EventDispatcher;
#[GraphQL\Entity]
class Artist
{
#[GraphQL\Field]
public $id;
#[GraphQL\Field]
public $name;
#[GraphQL\Association(filterCriteriaEventName: self::class . '.performances.filterCriteria')]
public $performances;
}
// Add a listener to your driver
$driver->get(EventDispatcher::class)->subscribeTo(
Artist::class . '.performances.filterCriteria',
function (Criteria $event): void {
$event->getCriteria()->andWhere(
$event->getCriteria()->expr()->eq('isDeleted', false)
);
},
);
You may modify the array used to define an entity type before it is created. This can be used for generated data and the like. You must attach to events before defining your GraphQL schema. See the detailed documentation for details.
use ApiSkeletons\Doctrine\ORM\GraphQL\Driver;
use ApiSkeletons\Doctrine\ORM\GraphQL\Event\EntityDefinition;
use App\ORM\Entity\Artist;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use League\Event\EventDispatcher;
$driver = new Driver($entityManager);
$driver->get(EventDispatcher::class)->subscribeTo(
Artist::class . '.definition',
static function (EntityDefinition $event): void {
$definition = $event->getDefinition();
// In order to modify the fields you must resovle the closure
$fields = $definition['fields']();
// Add a custom field to show the name without a prefix of 'The'
$fields['nameUnprefix'] = [
'type' => Type::string(),
'description' => 'A computed dynamically added field',
'resolve' => static function ($objectValue, array $args, $context, ResolveInfo $info): mixed {
return trim(str_replace('The', '', $objectValue->getName()));
},
];
$definition['fields'] = $fields;
}
);
Please read the detailed documentation.