This modules serves SilverStripe data as GraphQL representations, with helpers to generate schemas based on SilverStripe model introspection. It layers a pluggable schema registration system on top of the graphql-php library. The APIs are very similar, for example:
Require the composer package in your composer.json
composer require silverstripe/graphql
GraphQL is used through a single route which defaults to /graphql
. You need
to define Types and Queries to expose your data via this endpoint.
Code examples can be found in the examples/
folder (built out from the
configuration docs below).
Types describe your data. While your data could be any arbitrary structure, in
a SilverStripe project a GraphQL type usually relates to a DataObject
.
GraphQL uses this information to validate queries and allow GraphQL clients to
introspect your API capabilities. The GraphQL type system is hierarchical, so
the fields()
definition declares object properties as scalar types within
your complex type. Refer to the
graphql-php type definitions
for available types.
<?php
namespace MyProject\GraphQL;
use GraphQL\Type\Definition\Type;
use SilverStripe\GraphQL\TypeCreator;
use SilverStripe\GraphQL\Pagination\Connection;
class MemberTypeCreator extends TypeCreator
{
public function attributes()
{
return [
'name' => 'member'
];
}
public function fields()
{
return [
'ID' => ['type' => Type::nonNull(Type::id())],
'Email' => ['type' => Type::string()],
'FirstName' => ['type' => Type::string()],
'Surname' => ['type' => Type::string()],
];
}
}
Each type class needs to be registered with a unique name against the schema through YAML configuration:
SilverStripe\GraphQL:
schema:
types:
member: 'MyProject\GraphQL\MemberTypeCreator'
Types can be exposed via "queries". These queries are in charge of retrieving
data through the SilverStripe ORM. The response itself is handled by the
underlying GraphQL PHP library, which loops through the resulting DataList
and accesses fields based on the referred "type" definition.
<?php
namespace MyProject\GraphQL;
use GraphQL\Type\Definition\Type;
use SilverStripe\GraphQL\QueryCreator;
use MyProject\MyDataObject;
use SilverStripe\Security\Member;
class ReadMembersQueryCreator extends QueryCreator
{
public function attributes()
{
return [
'name' => 'readMembers'
];
}
public function args() {
return [
'Email' => ['type' => Type::string()]
];
}
public function type()
{
// Return a "thunk" to lazy load types
return function() {
return Type::listOf($this->manager->getType('member'));
};
}
public function resolve($args)
{
$list = Member::get();
// Optional filtering by properties
if(isset($args['Email'])) {
$list = $list->filter('Email', $args['Email']);
}
return $list;
}
}
We'll register the query with a unique name through YAML configuration:
SilverStripe\GraphQL:
schema:
queries:
readMembers: 'MyProject\GraphQL\ReadMembersQueryCreator'
You can query data with the following URL:
/graphql?query=query+readMembers{members{ID,Email,FirstName,Surname}}
The query contained in the query
parameter can be reformatted as follows:
query {
readMembers {
ID
Email
FirstName
Surname
}
}
The GraphQL module also provides a wrapper to return paginated and sorted records using offset based pagination.
This module currently does not support Relay (cursor based) pagination. This blog post describes the differences.
To have a Query
return a page-able list of records queries should extend the
PaginatedQueryCreator
class and return a Connection
instance.
<?php
namespace MyProject\GraphQL;
use GraphQL\Type\Definition\Type;
use SilverStripe\GraphQL\QueryCreator;
use SilverStripe\Security\Member;
use SilverStripe\GraphQL\Pagination\Connection;
use SilverStripe\GraphQL\Pagination\PaginatedQueryCreator;
use SilverStripe\GraphQL\Manager;
class ReadMembersQueryCreator extends PaginatedQueryCreator
{
public function connection() {
return Connection::create('readMembers')
->setConnectionType(function() {
return $this->manager->getType('member');
})
->setArgs([
'Email' => [
'type' => Type::string()
]
])
->setSortableFields(['ID', 'FirstName', 'Email'])
->setConnectionResolver(function($obj, $args) {
$list = Member::get();
// Optional filtering by properties
if(isset($args['Email'])) {
$list = $list->filter('Email', $args['Email']);
}
return $list;
});
}
}
Using a Connection
the GraphQL server will return the results wrapped under
the edges
result type. Connection
supports the following arguments:
limit
offset
sortBy
Additional arguments can be added by providing the setArgs
function (such as
Email
in the previous example). Each argument must be given a specific type.
Pagination information is provided under the pageInfo
type. This object type
supports the following fields:
totalCount
returns the total number of items in the list,hasNextPage
returns whether more records are available.hasPreviousPage
returns whether more records are available by decreasing the offset.
query Members {
readMembers(limit:1,offset:0) {
edges {
node {
ID
FirstName
Email
}
}
pageInfo {
hasNextPage
hasPreviousPage
totalCount
}
}
}
To limit the ability for users to perform searching and ordering as they wish,
Collection
instances can define their own limits and defaults.
setSortableFields
an array of allowed sort columns.setDefaultLimit
integer for the default page length (default 100)setMaximumLimit
integer for the maximumlimit
records per page to prevent excessive load trying to load millions of records (default 100)
return Connection::create('readMembers')
...
->setDefaultLimit(10)
->setMaximumLimit(100); // previous users requesting more than 100 records
Connection
can be used to return related objects such as has_many
and
many_many
models.
<?php
namespace MyProject\GraphQL;
use GraphQL\Type\Definition\Type;
use SilverStripe\GraphQL\TypeCreator;
use SilverStripe\GraphQL\Pagination\Connection;
class MemberTypeCreator extends TypeCreator
{
public function attributes()
{
return [
'name' => 'member'
];
}
public function fields()
{
$groupsConnection = Connection::create('Groups')
->setConnectionType(function() {
return $this->manager->getType('group');
})
->setDescription('A list of the users groups')
->setSortableFields(['ID', 'Title']);
return [
'ID' => ['type' => Type::nonNull(Type::id())],
'Email' => ['type' => Type::string()],
'FirstName' => ['type' => Type::string()],
'Surname' => ['type' => Type::string()],
'Groups' => [
'type' => $groupsConnection->toType(),
'args' => $groupsConnection->args(),
'resolve' => function($obj, $args) use ($groupsConnection) {
return $groupsConnection->resolveList(
$obj->Groups(),
$args
);
}
]
];
}
}
query Members {
readMembers(limit: 10) {
edges {
node {
ID
FirstName
Email
Groups(sortBy:[{field: "Title", direction:DESC}]) {
edges {
node {
ID
Title
Description
}
}
pageInfo {
hasNextPage
hasPreviousPage
totalCount
}
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
totalCount
}
}
}
A "mutation" is a specialised GraphQL query which has side effects on your data,
such as create, update or delete. Each of these operations would be expressed
as its own mutation class. Returning an object from the resolve()
method
will automatically include it in the response.
<?php
namespace MyProject\GraphQL;
use GraphQL\Type\Definition\Type;
use SilverStripe\GraphQL\MutationCreator;
use SilverStripe\Security\Member;
class CreateMemberMutationCreator extends MutationCreator
{
public function attributes()
{
return [
'name' => 'createMember',
'description' => 'Creates a member without permissions or group assignments'
];
}
public function type()
{
return function() {
return $this->manager->getType('member');
};
}
public function args()
{
return [
'Email' => ['type' => Type::nonNull(Type::string())],
'FirstName' => ['type' => Type::string()],
'LastName' => ['type' => Type::string()],
];
}
public function resolve($object, array $args, $context, $info)
{
if(!singleton(Member::class)->canCreate()) {
throw new \InvalidArgumentException('Member creation not allowed');
}
return (new Member($args))->write();
}
}
We'll register this mutation through YAML configuration:
SilverStripe\GraphQL:
schema:
mutations:
createMember: 'MyProject\GraphQL\CreateMemberMutationCreator'
You can run a mutation with the following query:
mutation($Email:String!) {
createMember(Email:$Email) {
ID
}
}
This will create a new member with an email address, which you can pass in as
query variables: {"Email": "[email protected]"}
. It'll return the new ID
property of the created member.
TODO
TODO
This module comes bundled with an implementation of graphiql, an in-browser IDE for GraphQL servers. It provides browse-able documentation of your schema, as well as auto complete and syntax-checking of your queries.
This tool is available in dev mode only. It can be accessed at
/dev/graphiql/
.
- Permission checks
- Input/constraint validation on mutations (with third-party validator)
- CSRF protection (or token-based auth)
- Generate CRUD operations based on DataObject reflection
- Generate DataObject relationship CRUD operations
- Create Enum GraphQL types from DBEnum
- Date casting
- Schema serialisation/caching (performance)