Pure PHP realization of GraphQL protocol

Overview

Looking for Maintainers!

Unfortunatelly, we cannot longer support this package and are looking for someone to take the ownership. Currently Only PRs with bugfixes and not breaking BC are being merged. It's very sad to acknowledge this, but we hope that someone can take it further with the community.

Please, PM @viniychuk if you are interested in taking over.

GraphQL

Join the chat at https://gitter.im/Youshido/GraphQL Latest Stable Version Build Status Scrutinizer Code Quality Code Coverage SensioLabsInsight

This is a pure PHP realization of the GraphQL protocol based on the working draft of the official GraphQL Specification located on http://facebook.github.io/graphql/.

GraphQL is a query language for APIs. It brings a new paradigm to the world of client-server communication and delivers a much more predictable behavior and smallest possible over-the-wire responses to any request. GraphQL advanced in many ways and has fundamental quality improvements:

  • strongly typed communication protocol makes both client and server predictable and more stable
  • encourages you to build a constantly evolving APIs and not use versions in the endpoints
  • bulk requests and responses to avoiding waiting for multiple HTTP handshakes
  • easily generated documentation and incredibly intuitive way to explore created API
  • clients will be much less likely to require backend changes

Current package is and will be trying to be kept up to date with the latest revision of the official GraphQL Specification which is now of April 2016.

Symfony bundle is available by the link – http://github.com/Youshido/GraphqlBundle

If you have any questions or suggestions – let's talk on GraphQL Gitter channel

Table of Contents

Getting Started

You should be better off starting with some examples and "Star Wars" become a somewhat "Hello world" for the GraphQL implementations. If you're looking just for that – you can get it via this link – Star Wars example. On the other hand, we prepared a step-by-step guide for those who wants to get up to speed bit by bit.

Installation

Install GraphQL package using composer. If you're not familiar with it, you should check out their manual. Run composer require youshido/graphql.

Alternatively you can run the following commands:

mkdir graphql-test && cd graphql-test
composer init -n
composer require youshido/graphql

Now you're ready to create your GraphQL Schema and check if everything works fine. Your first GraphQL app will be able to receive currentTime request and response with a formatted time string.

you can find this example in the examples directory – 01_sandbox.

Create an index.php file with the following content:

<?php
namespace Sandbox;

use Youshido\GraphQL\Execution\Processor;
use Youshido\GraphQL\Schema\Schema;
use Youshido\GraphQL\Type\Object\ObjectType;
use Youshido\GraphQL\Type\Scalar\StringType;

require_once 'vendor/autoload.php';

$processor = new Processor(new Schema([
    'query' => new ObjectType([
        'name' => 'RootQueryType',
        'fields' => [
            'currentTime' => [
                'type' => new StringType(),
                'resolve' => function() {
                    return date('Y-m-d H:ia');
                }
            ]
        ]
    ])
]));

$processor->processPayload('{ currentTime }');
echo json_encode($processor->getResponseData()) . "\n";

You can now execute php index.php and get a response with your current time:

{
   data: { currentTime: "2016-05-01 19:27pm" }
}

Just like that, you have created a GraphQL Schema with a field currentTime of type String and resolver for it. Don't worry if you don't know what the field, type and resolver mean here, you'll learn along the way.

If you're having any troubles – here're some troubleshooting points:

  • check that you have the latest composer version (composer self-update)
  • make sure your index.php file has been created in the same directory that you have vendor folder in (presumably it's graphql-test folder)
  • last but not least, check that you have php-cli installed and running and it's version >= 5.5 (php -v)

Also, you can always check if script from the examples folder work.

Tutorial – Creating Blog Schema

For our learning example we'll architect a GraphQL Schema for a Blog. You'll probably be using our package along with your favorite framework (we have a Symfony version here), but for the purpose of this tutorial we're keeping it all examples as plain php code.

(Complete example of the Blog schema available by the following link https://github.com/Youshido/GraphQL/tree/master/examples/02_blog)

Our Blog will have Users who can write Posts and leave Comments. Also, there will be a LikePost operation that could be performed by anyone. Let's start with Post. Take a look at the query that returns title and summary of the latest Post:

GraphQL query is a simple text query structured very much similar to the json format.

latestPost {
    title,
    summary
}

Supposedly server should reply with a relevant json response:

{
   data: {
       latestPost: {
           title: "This is a post title",
           summary: "This is a post summary"
       }
   }
}

It looks very simple and straight forward, so let's go ahead and write code that can handle this request.

Creating Post schema

We'll take a quick look on different approaches you can use to define your schema. Each of them has it's own pros and cons, inline approach might seem to be easier and faster when object oriented gives you more flexibility and freedom as your project grows. You should definitely use OOP approach every time you can reuse the type you're creating.

We're going to create RootQueryType with one field latestPost. Every GraphQL Field has a type(e.g. String, Int, Boolean) and it could be of a different kind(e.g. Scalar, Enum, List). You can read more about it in the official documentation, but for now you can think of field of a type like about instance of a class.

Inline approach

You can create inline-index.php file in your project folder and paste the following code there

inline-index.php

<?php
namespace InlineSchema;

use Youshido\GraphQL\Execution\Processor;
use Youshido\GraphQL\Schema\Schema;
use Youshido\GraphQL\Type\Object\ObjectType;
use Youshido\GraphQL\Type\Scalar\StringType;

// including autoloader
require_once __DIR__ . '/vendor/autoload.php';

// instantiating Processor and setting the schema
$processor = new Processor(new Schema([
    'query' => new ObjectType([
        // root query by convention has a name RootQueryType
        'name'   => 'RootQueryType',
        'fields' => [
            'latestPost' => [
                'type'    => new ObjectType([ // Post type is being created as ObjectType
                    'name'    => 'Post', // name of our type – "Post"
                    'fields'  => [
                        'title'   => new StringType(),  // defining "title" field, type - String
                        'summary' => new StringType(),  // defining "summary" field, type - String
                    ],
                ]),
                'resolve' => function () {          // resolver for latestPost field
                    return [                        // for now it returns a static array with data
                        "title"   => "New approach in API has been revealed",
                        "summary" => "In two words - GraphQL Rocks!",
                    ];
                }
            ]
        ]
    ])
]));

// creating payload and running it through processor
$payload = '{ latestPost { title, summary } }';
$processor->processPayload($payload);
// displaying result
echo json_encode($processor->getResponseData()) . "\n";

To check if everything is working – execute inline-index.php: php inline-index.php You should see response as the json encoded object latestPost inside the data section:

{
   data: {
       latestPost: {
           title: "New approach in API has been revealed",
           summary: "In two words - GraphQL Rocks!"
       }
   }
}

Try to play with the code by removing one field from the request or by changing the resolve function.

Object oriented approach

It's a common situation when you need to use the same custom type in different places, so we're going to create a separate class for the PostType and use it in our GraphQL Schema. To keep everything structured we're going to put this and all our future classes into the Schema folder.

Create a file Schema/PostType.php and put the following code in there:

<?php
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Object\AbstractObjectType;
use Youshido\GraphQL\Type\Scalar\StringType;

class PostType extends AbstractObjectType   // extending abstract Object type
{

    public function build($config)  // implementing an abstract function where you build your type
    {
        $config
            ->addField('title', new StringType())       // defining "title" field of type String
            ->addField('summary', new StringType());    // defining "summary" field of type String
    }

    public function getName()
    {
        return "Post";  // if you don't do getName – className without "Type" will be used
    }

}

Now let's create the main entry point for this example – index.php:

<?php

namespace Examples\Blog;

use Examples\Blog\Schema\PostType;
use Youshido\GraphQL\Execution\Processor;
use Youshido\GraphQL\Schema\Schema;
use Youshido\GraphQL\Type\Object\ObjectType;

require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/Schema/PostType.php';       // including PostType definition

$rootQueryType = new ObjectType([
    'name' => 'RootQueryType',
    'fields' => [
        'latestPost' => [
            'type'    => new PostType(),
            'resolve' => function ($source, $args, $info)
            {
                return [
                    "title"   => "New approach in API has been revealed",
                    "summary" => "In two words - GraphQL Rocks!",
                ];
            }
        ]
    ]
]);

$processor = new Processor(new Schema([
    'query' => $rootQueryType
]));
$payload = '{ latestPost { title, summary } }';

$processor->processPayload($payload);
echo json_encode($processor->getResponseData()) . "\n";

Ensure everything is working properly by running php index.php. You should see the same response you saw for the inline approach.

Next step would be to create a separate class for the latestPostField by extending AbstractField class: Schema/LatestPostField.php

<?php

namespace Examples\Blog\Schema;

use Youshido\GraphQL\Execution\ResolveInfo;
use Youshido\GraphQL\Field\AbstractField;

class LatestPostField extends AbstractField
{
    public function getType()
    {
        return new PostType();
    }

    public function resolve($value, array $args, ResolveInfo $info)
    {
        return [
            "title"   => "New approach in API has been revealed",
            "summary" => "In two words - GraphQL Rocks!",
        ];
    }
}

And now we can update our index.php:

<?php

namespace Examples\Blog;

use Examples\Blog\Schema\LatestPostField;
use Youshido\GraphQL\Execution\Processor;
use Youshido\GraphQL\Schema\Schema;
use Youshido\GraphQL\Type\Object\ObjectType;

require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/Schema/PostType.php';       // including PostType definition
require_once __DIR__ . '/Schema/LatestPostField.php';

$rootQueryType = new ObjectType([
    'name' => 'RootQueryType',
    'fields' => [
        new LatestPostField()
    ]
]);

$processor = new Processor(new Schema([
    'query' => $rootQueryType
]));
$payload = '{ latestPost { title, summary } }';

$processor->processPayload($payload);
echo json_encode($processor->getResponseData()) . "\n";

Choosing approach for your project

We would recommend to stick to object oriented approach for the several reasons (that matter the most for the GraphQL specifically):

  • makes your Types reusable
  • adds an ability to refactor your schema using IDEs
  • autocomplete to help you avoid typos
  • much easier to navigate through your Schema when project grows

With that being said, we use inline approach a lot to explore and bootstrap ideas or to develop simple fields/resolver that are going to be used in one place only. With the inline approach you can be fast and agile in creating mock-data server to test your frontend or mobile client.

Use valid Names
We highly recommend to get familiar with the official GraphQL Specification Remember that valid identifier in GraphQL should follow the pattern /[_A-Za-z][_0-9A-Za-z]*/. That means any identifier should consist of a latin letter, underscore, or a digit and cannot start with a digit. Names are case sensitive

We'll continue to work on the Blog Schema to explore all essentials details of developing GraphQL server.

Query Documents

In GraphQL terms – query document describe a complete request received by GraphQL service. It contains list of Operations and Fragments. Both are fully supported by our PHP library. There are two types of Operations in GraphQL:

  • Query – a read only request that is not supposed to do any changes on the server
  • Mutation – a request that changes(mutate) data on the server followed by a data fetch

You've already seen examples of Query with latestPost and currentTime, so let's define a simple Mutation that will provide API to Like the Post. Here's sample request and response of likePost mutation:

request

mutation {
  likePost(id: 5)
}

response

{
  data: { likePost: 2 }
}

Any Operation has a response type and in this case the likePost mutation type is Int

Note, that the response type of this mutation is a scalar Int. Of course in real life you'll more likely have a response of type Post for such mutation, but we're going to implement code for a simple example above and even keep it inside index.php:

<?php

namespace Examples\Blog;

use Examples\Blog\Schema\LatestPostField;
use Youshido\GraphQL\Execution\Processor;
use Youshido\GraphQL\Schema\Schema;
use Youshido\GraphQL\Type\NonNullType;
use Youshido\GraphQL\Type\Object\ObjectType;
use Youshido\GraphQL\Type\Scalar\IntType;

require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/Schema/PostType.php';       // including PostType definition
require_once __DIR__ . '/Schema/LatestPostField.php';

$rootQueryType = new ObjectType([
    'name'   => 'RootQueryType',
    'fields' => [
        new LatestPostField()
    ]
]);

$rootMutationType = new ObjectType([
    'name'   => 'RootMutationType',
    'fields' => [
        // defining likePost mutation field
        'likePost' => [
            // we specify the output type – simple Int, since it doesn't have a structure
            'type'    => new IntType(),
            // we need a post ID and we set it to be required Int
            'args'    => [
                'id' => new NonNullType(new IntType())
            ],
            // simple resolve function that always returns 2
            'resolve' => function () {
                return 2;
            },
        ]
    ]
]);

$processor = new Processor(new Schema([
    'query'    => $rootQueryType,
    'mutation' => $rootMutationType
]));
$payload   = 'mutation { likePost(id: 5) }';

$processor->processPayload($payload);
echo json_encode($processor->getResponseData()) . "\n";

Run php index.php, you should see a valid response:

{"data":{"likePost":2}}

Now, let's make our likePost mutation to return the whole Post as a result. First, we'll add likesCount field to the PostType:

<?php
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Object\AbstractObjectType;
use Youshido\GraphQL\Type\Scalar\IntType;
use Youshido\GraphQL\Type\Scalar\StringType;

class PostType extends AbstractObjectType
{

    public function build($config)
    {
        // you can define fields in a single addFields call instead of chaining multiple addField()
        $config->addFields([
            'title'      => new StringType(),
            'summary'    => new StringType(),
            'likesCount' => new IntType()
        ]);
    }

    // Since our class named by a convention, we can remove getName() method
}

Secondly, modify resolve function in LatestPostField:

public function resolve($value, array $args, ResolveInfo $info)
{
    return [
        "title"      => "New approach in API has been revealed",
        "summary"    => "In two words - GraphQL Rocks!",
        "likesCount" => 2
    ];
}

Lastly, we're going to change Mutation Type from IntType to PostType and update the resolve function to be compliant with the the new type and update the request:

<?php
// ...
$rootMutationType = new ObjectType([
    'name'   => 'RootMutationType',
    'fields' => [
        'likePost' => [
            'type'    => new PostType(),
            'args'    => [
                'id' => new NonNullType(new IntType())
            ],
            'resolve' => function () {
                return [
                    'title'     => 'New approach in API has been revealed',
                    'summary'   => 'In two words - GraphQL Rocks!',
                    'likesCount' => 2
                ];
            },
        ]
    ]
]);
// ...
$payload   = 'mutation { likePost(id: 5) { title, likesCount } }';
//...

Execute php index.php, you should see title and likesCount in response. We can now try to use id: 5 that we're passing as a parameter to our mutation:

$rootMutationType = new ObjectType([
    'name'   => 'RootMutationType',
    'fields' => [
        'likePost' => [
            'type'    => new PostType(),
            'args'    => [
                'id' => new NonNullType(new IntType())
            ],
            'resolve' => function ($source, $args, $resolveInfo) {
                return [
                    'title'      => 'Title for the post #' . $args['id'], // we can be sure that $args['id'] is always set
                    'summary'    => 'In two words - GraphQL Rocks!',
                    'likesCount' => 2
                ];
            },
        ]
    ]
]);

Now you have a basic understanding of how queries and mutations are structured and ready to move on to the details of the GraphQL Type System and PHP-specific features of the GraphQL server architecture.

Type System

Type is an atom of definition in GraphQL Schema. Every field, object, or argument has a type. GraphQL is a strongly typed language. There are system types and custom types defined specifically for the application, in our app we'll have custom types Post, User, Comment, etc. Your custom types are usually built on top of GraphQL system types.

Scalar Types

List of GraphQL Scalar types:

  • Int
  • Float
  • String
  • Boolean
  • Id (serialized as String per spec)

In addition, we implemented some types that might be useful and which we're considering to be scalar as well:

  • Timestamp
  • DateTimeTz (» RFC 2822 formatted date with TimeZone)

Date and DateTime are deprecated and will be remove. We're going to provide an easy solution how to replace them in your project

If you will ever need to define a new Scalar type, you can do that by extending from the AbstractScalarType class.

usage of scalar types will be shown in combination with other types down here

Objects

Every entity in your business logic will probably have a class that represents it's type. That class must be either extended from the AbstractObjectType or created as an instance of ObjectType. In our blog example we used ObjectType to create an inline PostType and extended AbstractObjectType to create a PostType class in the object oriented approach.

Let's take a closer look at the structure of PostType and see what parameters we can configure for each field.

<?php
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Object\AbstractObjectType;
use Youshido\GraphQL\Type\Scalar\BooleanType;
use Youshido\GraphQL\Type\Scalar\IntType;
use Youshido\GraphQL\Type\Scalar\StringType;

class PostType extends AbstractObjectType
{

    public function build($config)
    {
        // you can define fields in a single addFields call instead of chaining multiple addField()
        $config->addFields([
            'title'      => [
                'type' => new StringType(),
                'description'       => 'This field contains a post title',
                'isDeprecated'      => true,
                'deprecationReason' => 'field title is now deprecated',
                'args'              => [
                    'truncate' => new BooleanType()
                ],
                'resolve'           => function ($source, $args) {
                    return (!empty($args['truncate'])) ? explode(' ', $source['title'])[0] . '...' : $source['title'];
                }
            ],
            'summary'    => new StringType(),
            'likesCount' => new IntType()
        ]);
    }
}

Now you can change index.php to perform requests like these:

$payload   = 'mutation { likePost(id: 5) { title(truncate: true), likesCount } }';

As you can see we now have argument id for the mutation and another argument truncate for the field title inside PostTitle. We can use it everywhere that PostType is being used.

Interfaces

GraphQL supports Interfaces. You can define Interface and use it as a Type of an item in the List, or use Interface to make sure that specific objects certainly have fields you need. Each InterfaceType has to have at least one defined field and resolveType function. That function will be used to determine what exact Type will be returned by GraphQL resolver. Let's create a ContentBlockInterface that can represent a piece of content for the web page that have a title and a summary (just like our post earlier).

<?php
/**
 * ContentBlockInterface.php
 */
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\InterfaceType\AbstractInterfaceType;
use Youshido\GraphQL\Type\NonNullType;
use Youshido\GraphQL\Type\Scalar\StringType;

class ContentBlockInterface extends AbstractInterfaceType
{
    public function build($config)
    {
        $config->addField('title', new NonNullType(new StringType()));
        $config->addField('summary', new StringType());
    }

    public function resolveType($object) {
        // since there's only one type right now this interface will always resolve PostType
        return new PostType();
    }
}

Most often you'll be using only the build method to define fields and that need to be implemented. In order to associate this Interface to the PostType we have to override it's getInterfaces method:

<?php
/**
* PostType.php
*/
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Object\AbstractObjectType;
use Youshido\GraphQL\Type\Scalar\IntType;
use Youshido\GraphQL\Type\Scalar\StringType;

class PostType extends AbstractObjectType
{

    public function build($config)
    {
        $config->addFields([
            'title'      => new StringType(),
            'summary'    => new StringType(),
            'likesCount' => new IntType()
        ]);
    }

    public function getInterfaces()
    {
        return [new ContentBlockInterface()];
    }
}

As you might have noticed there's no getName method in both Interface and Type classes – that's a simplified approach available when you want to have your name exactly the same as the class name without the Type at the end.

If you run the script as it is right now – php index.php, you should get an error:

{"errors":[{"message":"Implementation of ContentBlockInterface is invalid for the field title"}]}

You've got this error because the title field definition in the PostType is different from the one described in the ContentBlockInterface. To fix it we have to declare fields that exist in the Interface with the same names and types. We already have title but it's a nullable field so we have to change it by adding a non-null wrapper – new NonNullType(new StringType()). You can check the result by executing index.php script again, you should get the usual response.

For the convenience we also created $config->applyInterface() method that could be inside build():

<?php
/**
 * PostType.php
 */
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Object\AbstractObjectType;
use Youshido\GraphQL\Type\Scalar\IntType;

class PostType extends AbstractObjectType
{

    public function build($config)
    {
        $config->applyInterface(new ContentBlockInterface());
        $config->addFields([
            'likesCount' => new IntType()
        ]);
    }

    public function getInterfaces()
    {
        return [new ContentBlockInterface()];
    }
}

Enums

GraphQL Enums are the variation on the Scalar type, which represents one of the predefined values. Enums serialize as a string: the name of the represented value but can be associated with a numeric (as an example) value.

To show you how Enums work we're going to create a new class - PostStatus:

<?php
/**
 * PostStatus.php
 */
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Enum\AbstractEnumType;

class PostStatus extends AbstractEnumType
{
    public function getValues()
    {
        return [
            [
                'value' => 0,
                'name'  => 'DRAFT',
            ],
            [
                'value' => 1,
                'name'  => 'PUBLISHED',
            ]
        ];
    }
}

Now, add a status field to the PostType:

<?php
/**
 * PostType.php
 */
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\NonNullType;
use Youshido\GraphQL\Type\Object\AbstractObjectType;
use Youshido\GraphQL\Type\Scalar\IntType;
use Youshido\GraphQL\Type\Scalar\StringType;

class PostType extends AbstractObjectType
{

    public function build($config)
    {
        $config->addFields([
            'title'      => new NonNullType(new StringType()),
            'summary'    => new StringType(),
            'likesCount' => new IntType(),
            'status'     => new PostStatus()
        ]);
    }

    public function getInterfaces()
    {
        return [new ContentBlockInterface()];
    }
}

and update the resolve function inside latestPost field:

<?php

namespace Examples\Blog\Schema;

use Youshido\GraphQL\Execution\ResolveInfo;
use Youshido\GraphQL\Field\AbstractField;

class LatestPostField extends AbstractField
{
    public function getType()
    {
        return new PostType();
    }

    public function resolve($value, array $args, ResolveInfo $info)
    {
        return [
            "title"      => "New approach in API has been revealed",
            "summary"    => "In two words - GraphQL Rocks!",
            "status"     => 1,
            "likesCount" => 2
        ];
    }
}

Request the status field in your query:

$payload  = '{ latestPost { title, status, likesCount } }';

You should get a result similar to the following:

{"data":{"latestPost":{"title":"New approach in API has been revealed","status":"PUBLISHED"}}}

Unions

GraphQL Unions represent an object type that could be resolved as one of a specified GraphQL Object types. To get you an idea of what this is we're going to create a new query field that will return a list of unions (and get to the ListType after it).

You can consider Union as a combined type that is needed mostly when you want to have a list of different objects

Imaging that you have a page and you need to get all content blocks for this page. Let content block be either Post or Banner. Create a BannerType:

<?php
/**
 * BannerType.php
 */
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Object\AbstractObjectType;
use Youshido\GraphQL\Type\Scalar\StringType;

class BannerType extends AbstractObjectType
{
    public function build($config)
    {
        $config
            ->addField('title', new StringType())
            ->addField('imageLink', new StringType());
    }
}

Now let's combine the Banner type and the Post type to create a ContentBlockUnion that will extend an AbstractUnionType. Each UnionType needs to define a list of types it unites by implementing the getTypes method and the resolveType method to resolve object that will be returned for each instance of the Union.

<?php
/**
 * ContentBlockUnion.php
 */

namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Union\AbstractUnionType;

class ContentBlockUnion extends AbstractUnionType
{
    public function getTypes()
    {
        return [new PostType(), new BannerType()];
    }

    public function resolveType($object)
    {
        // we simple look if there's a "post" inside the object id that it's a PostType otherwise it's a BannerType
        return empty($object['id']) ? null : (strpos($object['id'], 'post') !== false ? new PostType() : new BannerType());
    }
}

We're also going to create a simple DataProvider that will give us test data to operate with:

<?php
/**
 * DataProvider.php
 */
namespace Examples\Blog\Schema;

class DataProvider
{
    public static function getPost($id)
    {
        return [
            "id"        => "post-" . $id,
            "title"     => "Post " . $id . " title",
            "summary"   => "This new GraphQL library for PHP works really well",
            "status"    => 1,
            "likesCount" => 2
        ];
    }

    public static function getBanner($id)
    {
        return [
            'id'        => "banner-" . $id,
            'title'     => "Banner " . $id,
            'imageLink' => "banner" . $id . ".jpg"
        ];
    }
}

Now, we're ready to update our Schema and include ContentBlockUnion into it. As we're getting our schema bigger we'd like to extract it to a separate file as well:

<?php
/**
 * BlogSchema.php
 */
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Config\Schema\SchemaConfig;
use Youshido\GraphQL\Schema\AbstractSchema;
use Youshido\GraphQL\Type\ListType\ListType;

class BlogSchema extends AbstractSchema
{
    public function build(SchemaConfig $config)
    {
        $config->getQuery()->addFields([
            new LatestPostField(),
            'randomBanner'     => [
                'type'    => new BannerType(),
                'resolve' => function () {
                    return DataProvider::getBanner(rand(1, 10));
                }
            ],
            'pageContentUnion' => [
                'type'    => new ListType(new ContentBlockUnion()),
                'resolve' => function () {
                    return [DataProvider::getPost(1), DataProvider::getBanner(1)];
                }
            ]
        ]);
        $config->getMutation()->addFields([
            new LikePostField()
        ]);
    }

}

Having this separate schema file you should update your index.php to look like this:

<?php

namespace Examples\Blog;

use Examples\Blog\Schema\BlogSchema;
use Youshido\GraphQL\Execution\Processor;

require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/Schema/PostType.php';
require_once __DIR__ . '/Schema/LatestPostField.php';
require_once __DIR__ . '/Schema/ContentBlockInterface.php';
require_once __DIR__ . '/Schema/PostStatus.php';
require_once __DIR__ . '/Schema/LikePostField.php';
require_once __DIR__ . '/Schema/BlogSchema.php';
require_once __DIR__ . '/Schema/ContentBlockUnion.php';
require_once __DIR__ . '/Schema/BannerType.php';
require_once __DIR__ . '/Schema/DataProvider.php';

$processor = new Processor(new BlogSchema());
$payload  = '{ pageContentUnion { ... on Post { title } ... on Banner { title, imageLink } } }';


$processor->processPayload($payload);
echo json_encode($processor->getResponseData()) . "\n";

Due to the GraphQL syntax you have to specify fields for each type of object you're getting in the union request, if you're not familiar with it read more at official documentation If everything was done right you should see the following response:

{"data":{"pageContentUnion":[
  {"title":"Post 1 title"},
  {"title":"Banner 1","imageLink":"banner1.jpg"}
]}}

Also, you might want to check out how to use GraphiQL tool to get a better visualization of what you're doing here.

Lists

As you've seen in the previous example ListType is used to create a list of any items that are or extend GraphQL type. List type can be also created by using InterfaceType as an item which gives you flexibility in defining your schema. Let's go ahead and add ListType field to our BlogSchema.

<?php
/**
 * BlogSchema.php
 */
namespace Examples\Blog\Schema;

use Youshido\GraphQL\Config\Schema\SchemaConfig;
use Youshido\GraphQL\Schema\AbstractSchema;
use Youshido\GraphQL\Type\ListType\ListType;

class BlogSchema extends AbstractSchema
{
    public function build(SchemaConfig $config)
    {
        $config->getQuery()->addFields([
            new LatestPostField(),
            'randomBanner'     => [
                'type'    => new BannerType(),
                'resolve' => function () {
                    return DataProvider::getBanner(rand(1, 10));
                }
            ],
            'pageContentUnion' => [
                'type'    => new ListType(new ContentBlockUnion()),
                'resolve' => function () {
                    return [DataProvider::getPost(1), DataProvider::getBanner(1)];
                }
            ],
            'pageContentInterface' => [
                'type'    => new ListType(new ContentBlockInterface()),
                'resolve' => function () {
                    return [DataProvider::getPost(2), DataProvider::getBanner(3)];
                }
            ]
        ]);
        $config->getMutation()->addFields([
            new LikePostField()
        ]);
    }

}

We've added a pageContentInterface field that have a ListType of ContentBlockInterface.
Resolve function returns list which consists of one Post and one Banner. To test it we'll modify our payload to the following one:

<?php
$payload  = '{ pageContentInterface { title} }';

Be aware, because BannerType doesn't implement ContentBlockInterface you would get an error:

{ "errors": [ "message": "Type Banner does not implement ContentBlockInterface" } ]}

To fix this we just need to add ContentBlockInterface by implementing getInterfaces method and adding the proper field definitions to our BannerType:

<?php
/**
 * BannerType.php
 */

namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Config\TypeConfigInterface;
use Youshido\GraphQL\Type\NonNullType;
use Youshido\GraphQL\Type\Object\AbstractObjectType;
use Youshido\GraphQL\Type\Scalar\StringType;

class BannerType extends AbstractObjectType
{
    public function build($config)
    {
        $config
            ->addField('title', new NonNullType(new StringType()))
            ->addField('summary', new StringType())
            ->addField('imageLink', new StringType());
    }

    public function getInterfaces()
    {
        return [new ContentBlockInterface()];
    }
}

Send the request again and you'll get a nice response with titles of the both Post and Banner:

{
  "data": {
    "pageContentInterface":[
      {"title":"Post 2 title"},
      {"title":"Banner 3"}
    ]
  }
}

Input Objects

So far we've been working mostly on the requests that does not require you to send any kind of data other than a simple Int, but in real life you'll have a lot of requests (mutations) where you'll be sending to server all kind of forms – login, registration, create post and so on. In order to properly handle and validate that data GraphQL type system provides an InputObjectType class.

By default all the Scalar types are inputs but if you want to have a single more complicated input type you need to extend an InputObjectType.

Let's develop a PostInputType that could be used to create a new Post in our system.

<?php
/**
 * PostInputType.php
 */

namespace Examples\Blog\Schema;

use Youshido\GraphQL\Type\Config\InputTypeConfigInterface;
use Youshido\GraphQL\Type\NonNullType;
use Youshido\GraphQL\Type\InputObject\AbstractInputObjectType;
use Youshido\GraphQL\Type\Scalar\StringType;

class PostInputType extends AbstractInputObjectType
{

    public function build($config)
    {
        $config
            ->addField('title', new NonNullType(new StringType()))
            ->addField('summary', new StringType());
    }

}

This InputType could be used to create a new mutation (we can do it in the BlogSchema::build for testing):

<?php
// BlogSchema->build() method
$config->getMutation()->addFields([
    'likePost'   => new LikePost(),
    'createPost' => [
        'type'   => new PostType(),
        'args' => [
            'post'   => new PostInputType(),
            'author' => new StringType()
        ],
        'resolve' => function($value, array $args, ResolveInfo $info) {
            // code for creating a new post goes here
            // we simple use our DataProvider for now
            $post = DataProvider::getPost(10);
            if (!empty($args['post']['title'])) $post['title'] = $args['post']['title'];
            return $post;
        }
    ]
]);

Try to execute the following mutation so you can see the result:

mutation {
  createPost(author: "Alex", post: {title: "Hey, this is my new post", summary: "my post" }) {
    title
  }
}

result:

{"data":{"createPost":{"title":"Hey, this is my new post"}}}

The best way to see the result of your queries/mutations and to inspect the Schema is to use a GraphiQL tool

Non Null

NonNullType is really simple to use – consider it as a wrapper that can ensure that your field / argument is required and being passed to the resolve function. We have used NonNullType couple of times already so we'll just show you useful methods that that could be called on NonNullType objects:

  • getNullableType()
  • getNamedType()

These two can return you a type that was wrapped up in the NonNullType so you can get it's fields, arguments or name.

Building your schema

It's always a good idea to give you a heads up about any possible errors as soon as possible, better on the development stage. For this purpose specifically we made a lot of Abstract classes that will force you to implement the right methods to reduce amount of errors or if you're lucky enough – to have no errors at all.

Abstract type classes

If you want to implement a new type consider extending the following classes:

  • AbstractType
  • AbstractScalarType
  • AbstractObjectType
  • AbstractMutationObjectType
  • AbstractInputObjectType
  • AbstractInterfaceType
  • AbstractEnumType
  • AbstractListType
  • AbstractUnionType
  • AbstractSchemaType

Mutation helper class

You can create a mutation by extending AbstractObjectType or by creating a new field of ObjectType inside your Schema::build method. It is crucial for the class to have a getType method returning the actual OutputType of your mutation but it couldn't be implemented as abstract method, so we created a wrapper class called AbstractMutationObjectType. This abstract class can help you to not forget about OutputType by forcing you to implement a method getOutputType that will eventually be used by internal getType method.

Useful information

This section will be updating on a regular basis with the useful links and references that might help you to quicker become a better GraphQL developer.

GraphiQL Tool

To improve our testing experience even more we suggest to start using GraphiQL client, that's included in our examples. It's a JavaScript GraphQL Schema Explorer. To use it – run the server.sh from the examples/02_blog/ folder and open the examples/GraphiQL/index.html file in your browser. You'll see a nice looking editor that has an autocomplete function and contains all information about your current Schema on the right side in the Docs sidebar: GraphiQL Interface

Comments
  • handle deferred resolvers

    handle deferred resolvers

    This landed over in webonyx: https://github.com/webonyx/graphql-php/blob/c7688c92499fa2009a235cf2498f5582204ff7bf/docs/data-fetching.md#solving-n1-problem

    I think this is a really elegant solution. I was looking into strategies for doing this type of thing and was looking at some sort of analyze-query-via-lookahead optimizations, but this seems much cleaner (and easier to implement).

    enhancement help wanted 
    opened by roippi 18
  • Resolve function - Pass a bag of arguments instead of an array

    Resolve function - Pass a bag of arguments instead of an array

    Hello,

    Thanks for your great work!

    It would be really nice if the arguments passed to the resolve function were stored in a bag rather than a standard array. For example:

    We create an ArgumentBag class:

    class ArgumentBag implements \IteratorAggregate, \Countable
    {
      private $arguments;
    
      public function add(array $arguments = array())
      {
          $this->arguments = array_replace($this->arguments, $arguments);
      }
    
      public function get($key, $default = null)
      {
          return array_key_exists($key, $this->arguments) ? $this->arguments[$key] : $default;
      }
    
      public function has($key)
      {
          return array_key_exists($key, $this->$arguments);
      }
    
      public function set($key, $value)
      {
          $this->arguments[$key] = $value;
      }
    
      public function getIterator()
      {
          return new \ArrayIterator($this->arguments);
      }
    
      public function count()
      {
          return count($this->arguments);
      }
      
    }
    

    And we can pass this to the resolve function and have a more object-oriented way of dealing with parameters:

        public function resolve($value, ArgumentBag $args, ResolveInfo $info) {
            // get argument value if exists, otherwise return default of 10
           // nicer than doing isset($args['limit']) ? $args['limit'] : 10;
            $limit = $args->get('limit', 10);
            if($args->has('name')) {
                // do something with name...
            }
       }
    

    Let me know your thoughts...

    Thanks!

    enhancement 
    opened by jamhall 17
  • PhpStorm code inspection cleanups

    PhpStorm code inspection cleanups

    This fixes most issues, there are a few left, practically all stemming from the slightly sloppy usage of traits for fields and types. I figured it's not worth trying to work around those with phpdoc annotations, instead they should ideally be refactored somewhat.

    The most notable changes are:

    • https://github.com/digiaonline/GraphQL/commit/248f0ae44e6b2783d64ffe56933acec1c1249397 removes a new ResolveInfo call, may speed things up a tiny bit
    • https://github.com/digiaonline/GraphQL/commit/c422fc044408259ad512f579d9f4bc94f17df254 removes two unused traits. They have been marked as deprecated and the TODO says they should be removed in the next release, so I figured why not.
    opened by Jalle19 11
  • Fragment order with named fields returns invalid response

    Fragment order with named fields returns invalid response

    Query:

    query Queries {
      viewer {
        me {
          profilePhotoAlbum {
            ...albumInfo
          }
          ...userInfo // <== POSITION OF THIS IS IMPORTANT
        }
      }
    }
    fragment userInfo on User {
      profilePhotoAlbum {
        p1: id
      }
    }
    fragment albumInfo on PhotoAlbum {
      p2: id
    }
    

    Result:

    {
      "data": {
        "viewer": {
          "me": {
            "profilePhotoAlbum": {
              "p1": "UGhvdG9BbGJ1bToxMDAwMjUwNg==" // <== MISSING p2
            }
          }
        }
      }
    }
    

    Second query with moved userInfo fragment up

    query Queries {
      viewer {
        me {
          ...userInfo // <== POSITION OF THIS IS IMPORTANT
          profilePhotoAlbum {
            ...albumInfo
          }
        }
      }
    }
    fragment userInfo on User {
      profilePhotoAlbum {
        p1: id
      }
    }
    fragment albumInfo on PhotoAlbum {
      p2: id
    }
    

    Result:

    {
      "data": {
        "viewer": {
          "me": {
            "profilePhotoAlbum": {
              "p2": "UGhvdG9BbGJ1bToxMDAwMjUwNg==" // <== MISSING p1
            }
          }
        }
      }
    }
    

    Conclusion: Order of ragments contained named fields can cause invalid response. At both queries expected result was containing p1 and p2 but received only one. This problem is simplified as much as I can but can cause really big problems when used with relay

    opened by svrcekmichal 11
  • Should clearErrors() be called at the beginning of processPayload?

    Should clearErrors() be called at the beginning of processPayload?

    Hi, I'm not sure if it's an error. but I am calling processPayload more than once and it has a bad behaviour (IMHO) when an error occurs. If an error occurs and I call processPayload again that error will stick there and the second call will have that error on the response.

    The errors are being kept there (by the ErrorContainerTrait) and are not cleaned between two calls.

    I was able to "fix" it like this:

    $processor = $this->getService('graphql.processor');
    $processor->getExecutionContext()->clearErrors();
    $processor->processPayload($query);
    return $processor->getResponseData();
    

    But I think that you could call the clean errors on every processPayload call. At least this is what I can tell from my actual understanding of the API. In the case that a new processor should be created for every "request", so this might be an issue with the symfony bundle, that should declare the processor service with shared:false.

    enhancement 
    opened by brunoreis 10
  • Problem with passing InputObjectType as argument with relation to ListType

    Problem with passing InputObjectType as argument with relation to ListType

    Hi,

    I started to use Symfony bundle created by you. Thanks for committing into it.

    After some time I got to the problem which may be a bug.

    I prepared classes like that :

    class ItemType extends AbstractObjectType
    {
        public function build($config)
        {
            $config->addFields([
                'id'        => new NonNullType(new IdType()),
                'custom' => new CustomField(),
            ]);
        }
    }
    
    class CustomField extends AbstractContainerAwareField
    {
        public function build(FieldConfig $config)
        {
            $config->addArguments([
                'argX' => new NonNullType(new ArgXInputType())
            ]);
        }
    
        public function resolve($value, array $args, ResolveInfo $info)
        {
            return ['string' => 'some string'];
        }
    
        public function getType()
        {
            return new CustomType();
        }
    }
    
    class CustomType extends AbstractObjectType
    {
        public function build($config)
        {
            $config->addFields([
                'string' => new StringType(),
            ]);
        }
    }
    
    class ArgXInputType extends AbstractInputObjectType
    {
        public function build($config)
        {
            $config->addFields([
                'x' => new NonNullType(new BooleanType()),
            ]);
        }
    }
    

    now I have 2 fields one with single object type:

    class ItemField extends AbstractContainerAwareField
    {
        public function build(FieldConfig $config)
        {
            $config->addArguments([
                'id' => new NonNullType(new IdType())
            ]);
        }
    
        public function resolve($value, array $args, ResolveInfo $info)
        {
            return $this->container->get('some_repo')->find($args['id']);
        }
    
        public function getType()
        {
            return new ItemType();
        }
    }
    

    and list

    class ItemsField extends AbstractContainerAwareField
    {
        public function build(FieldConfig $config)
        {
            $config->addArguments([
                'example' => new StringType(),
            ]);
        }
    
        public function resolve($value, array $args, ResolveInfo $info)
        {
            return $this->container->get('some_repo')->findBy($args);
        }
    
        public function getType()
        {
            return new ListType(new ItemType());
        }
    }
    

    Now when I execute :

    {
      item(id:2) {
        id
        custom(argX: {x: true}) {
          string
        }
      }
    }
    

    I've got valid response :

    {
      "data": {
        "item": {
          "id": "2",
          "custom": {
            "string": "some string"
          }
        }
      }
    }
    

    but when I want to fetch list

    {
      items {
        id
        custom(argX: {x: true}) {
          string
        }
      }
    }
    

    I've got something like that :

    {
      "data": {
        "items": [
          {
            "id": "1",
            "custom": {
              "string": "some string"
            }
          },
          null,
          null,
          null,
          null,
          null,
          null,
          null,
          null,
          null
        ]
      },
      "errors": [
        {
          "message": "Not valid type for argument \"argX\" in query \"custom\"",
          "locations": [
            {
              "line": 8,
              "column": 16
            }
          ]
        },
        {
          "message": "Not valid type for argument \"argX\" in query \"custom\"",
          "locations": [
            {
              "line": 8,
              "column": 16
            }
          ]
        },
        {
          "message": "Not valid type for argument \"argX\" in query \"custom\"",
          "locations": [
            {
              "line": 8,
              "column": 16
            }
          ]
        },
        {
          "message": "Not valid type for argument \"argX\" in query \"custom\"",
          "locations": [
            {
              "line": 8,
              "column": 16
            }
          ]
        },
        {
          "message": "Not valid type for argument \"argX\" in query \"custom\"",
          "locations": [
            {
              "line": 8,
              "column": 16
            }
          ]
        },
        {
          "message": "Not valid type for argument \"argX\" in query \"custom\"",
          "locations": [
            {
              "line": 8,
              "column": 16
            }
          ]
        },
        {
          "message": "Not valid type for argument \"argX\" in query \"custom\"",
          "locations": [
            {
              "line": 8,
              "column": 16
            }
          ]
        },
        {
          "message": "Not valid type for argument \"argX\" in query \"custom\"",
          "locations": [
            {
              "line": 8,
              "column": 16
            }
          ]
        },
        {
          "message": "Not valid type for argument \"argX\" in query \"custom\"",
          "locations": [
            {
              "line": 8,
              "column": 16
            }
          ]
        }
      ]
    }
    

    So as u see for the first row argument is valid, for the rest arguments are empty.

    I didnt try to solve it by mutation as my operation doesnt modify data, it just calculates price base on object passed as argument. It seems to be valid graphql query ? When I operate on ex. StringType as argument instead of ObjectInputType (ArgXInputType in this example) it works fine with List.

    I will try to dig into the code but its my first day with this lib so it may take a while.

    opened by m-naw 10
  • Query using Fragment for Union gives empty array

    Query using Fragment for Union gives empty array

    When using a Fragment when querying for a Union, you get an empty array As you can see on Launchpad, this should work. 😕

    schema:

    type Query {
      getAllPersons: PersonReturnType
    }
    
    type PersonReturnType {
      persons: [Person],
    }
    	
    union Person = Instructor | Student
    
    type Instructor {
      id: Int
      name: String
      salary: Float
    }
    
    type Student {
      id: Int
      name: String
      major: String
    }
    

    query:

    {
      getAllPersons {
        persons {
          ...personFrag
        }
      }
    }
    
    fragment personFrag on Person {
      __typename
      id
      name
      
      ... on Instructor {
        salary
      }
      
      ... on Student {
        major
      }
    }
    

    response:

    {
      "data": {
        "getAllPersons": {
          "persons": []
        }
      }
    }
    

    CC: @viniychuk

    bug simple task 
    opened by MichaelDeBoey 9
  • Error: Call to undefined method Youshido\GraphQL\Parser\Ast\Field::getFields()

    Error: Call to undefined method Youshido\GraphQL\Parser\Ast\Field::getFields()

    I have a Subtopic tree. If I declare it like this:

            $config->addFields([
                'id' => new NonNullType(new IdType()),
                'name' => [
                    'type' => new StringType(),
                    'description' => 'Nome/Título do subtópico'
                ],
                'canHaveItems' => [
                    'type' => new BooleanType(),
                    'description' => 'Can have Items'
                ],
                'subtopics' => [
                     'type' => new ListType(new SubtopicType()),
                     'description' => 'Subtópicos filhos, aninhados.'
                ]
                ,
                'ancestors' => [
                    'type' => new ListType(new StringType()),
                    'description' =>'Pais desse subtópico'
                ]
            ]);
    

    it works fine. But if I add another relation to a SubtopicType in the ancestors, like this:

            $config->addFields([
                'id' => new NonNullType(new IdType()),
                'name' => [
                    'type' => new StringType(),
                    'description' => 'Nome/Título do subtópico'
                ],
                'canHaveItems' => [
                    'type' => new BooleanType(),
                    'description' => 'Can have Items'
                ],
                'subtopics' => [
                     'type' => new ListType(new SubtopicType()),
                     'description' => 'Subtópicos filhos, aninhados.'
                ]
                ,
                'ancestors' => [
                    'type' => new ListType(new SubtopicType()),
                    'description' =>'Pais desse subtópico'
                ]
            ]);
    

    It's giving me this error:

    Error: Call to undefined method Youshido\GraphQL\Parser\Ast\Field::getFields()
    
    .../vendor/youshido/graphql/src/Execution/Processor.php:335
    .../vendor/youshido/graphql/src/Execution/Processor.php:324
    .../vendor/youshido/graphql/src/Execution/Processor.php:440
    .../vendor/youshido/graphql/src/Execution/Processor.php:193
    .../vendor/youshido/graphql/src/Execution/Processor.php:380
    .../vendor/youshido/graphql/src/Execution/Processor.php:324
    .../vendor/youshido/graphql/src/Execution/Processor.php:440
    .../vendor/youshido/graphql/src/Execution/Processor.php:193
    .../vendor/youshido/graphql/src/Execution/Processor.php:152
    .../vendor/youshido/graphql-bundle/Execution/Processor.php:70
    .../vendor/youshido/graphql/src/Execution/Processor.php:86
    .../vendor/youshido/graphql-bundle/Execution/Processor.php:63
    .../tests/AbstractIntegrationTestCase.php:101
    .../tests/AbstractIntegrationTestCase.php:116
    .../tests/AppBundle/GraphQL/Subtopics/Queries/SubtopicsTest.php:43
    
    
    opened by brunoreis 9
  • Bug: optional variable does not exist on query

    Bug: optional variable does not exist on query

    Query:

    query userById($offset:Int) {
      user(id: 2) {
        id
        photoAlbums(offset: $offset) {
          edges {
            id
          }
        }
      }
    }
    

    Response:

    {
      "data": {
        "user": {
          "id": 2,
          "photoAlbums": null
        }
      },
      "errors": [
        {
          "message": "Variable \"offset\" does not exist for query \"photoAlbums\""
        }
      ]
    }
    

    If i dont use named query

    {
      user(id: 2) {
        id
        photoAlbums {
          edges {
            id
          }
        }
      }
    }
    

    response is

    {
      "data": {
        "user": {
          "id": 2,
          "photoAlbums": {
            "edges": [
              {
                "id": 10000965
              },
              ....
            ]
          }
        }
      }
    }
    
    opened by svrcekmichal 9
  • DateTime/Enum types doesn't show invalid input data

    DateTime/Enum types doesn't show invalid input data

    Hi,

    If I have a param with DateTime and write a bad input, It doesnt show an error. And in resolver I get a null value...

    Example

    class PepField extends AbstractFieldMutation
    {
        public function buildParent(FieldConfig $config)
        {
            $config
                ->setDescription('Deactivates a user')
                ->addArguments([
                    'date'  => new DateTimeTzType(),
                ])
            ;
        }
    
        public function resolve($value, $args, ResolveInfo $info)
        {
    
            return true;
        }
    
        /**
         * @return AbstractObjectType|AbstractType
         */
        public function getType()
        {
            return new BooleanType();
        }
    }
    

    Execute with this query

    mutation{
      pep(date: "asdasd")
    }
    

    and result is

    {
      "data": {
        "pep": true
      }
    }
    

    Looking for this "error" I found the class ResolveValidator, lines 54 - 60

    if (!$argument->getType()->isValidValue($argumentType->parseValue($astArgument->getValue()))) {
         $error = $argument->getType()->getLastError();
         throw new ResolveException($error ? $error : sprintf('Not valid type for argument "%s" in query "%s"', $astArgument->getName(), $field->getName()), $astArgument->getLocation());
     }
    

    The problem here is the function parseValue return a null value and isValidValue verify input NULL instead of original value.

    SOLUTION I think inside the function "isValidValue" (AbstractType) should be call inside to "parseValue", to know the original value and if It get a NULL it will throw an error.

    if (!$argument->getType()->isValidValue($astArgument->getValue())) {
        $error = $argument->getType()->getLastError();
        throw new ResolveException($error ? $error : sprintf('Not valid type for argument "%s" in query "%s"', $astArgument->getName(), $field->getName()), $astArgument->getLocation());
    }
    

    and each Validator call to parseValue if it's necessary

    I can create a pull request if you give me the approval

    opened by MGDSoft 8
  • More realistic GraphQL vs REST explanation

    More realistic GraphQL vs REST explanation

    Hey there! I'm making this PR with the best of intentions, but I'm sure it'll be taken as aggressive. Not the goal.

    Recently I've been trying to explain to people the differences between GraphQL and REST and debunk some of the "GraphQL is magically better" stuff going around. Your introduction is furthering the issue.

    Generally, GraphQL and REST are just different, and pretending that one is better is going to confuse people.

    self-checks embedded on the ground level of your backend architecture

    What does this mean

    reusable API for different client versions and devices, i.e. no more need in maintaining "/v1" and "/v2"

    This was never recommended in REST, in fact the opposite was recommended. Evolvability is king for both.

    easily generated documentation and incredibly intuitive way to explore created API

    Yeah! Love it, but their schemas are based on concepts like JSON Schema, JSON LD, OpenAPI, etc, and exploring the API is what HATEOAS provides, which is a REST concept.

    once your architecture is complete – most client-based changes does not require backend modifications

    👍

    I had a go at making it a bit more accurate for you, I hope you like it!

    opened by philsturgeon 8
  • Parsing arrays of ENUMs doesn't work

    Parsing arrays of ENUMs doesn't work

    I have declared the type of an input to be [ENUM], hence I should be able to query it like this:

    query {
      someField(input: [enumValue]) {
        ...
      }
    }
    

    However it doesn't work, I get the following error message:

    {
      "errors": [
        {
          "message": "Can't parse argument",
          "location": {
            "line": ...,
            "column": ...      }
        }
      ]
    }
    

    Passing an ENUM value as an input works well, passing arrays of string works well, but passing arrays of ENUMs does not.

    opened by leoloso 0
  • Can extract the Parser as a standalone package?

    Can extract the Parser as a standalone package?

    Hi! I'm currently using only the Parser from Youshido, and not the full GraphQL server. I use it to power another GraphQL server in PHP which I'm developing, GraphQL by PoP (I'm in the process of launching it, soon).

    Would it be possible to extract the Parser as a standalone solution, and then have the Youshido server simply include it as a dependency?

    I know that this is not really needed, but it makes the whole solution nicer and cleaner, and this parser works really well as a standalone solution. (I also tried the one from webonyx, but that one doesn't work in my project, because it has a hard dependency on its own schema)

    Also, I've seen that this package is looking for new maintainers, and I wonder if it would be easier to find them for smaller tasks. Then, someone could become the maintainer just for the parser and keep it updated with all the upcoming changes to the GraphQL spec, which are already happening (eg: the "&" symbol to declare all implemented interfaces).

    Thanks

    opened by leoloso 2
  • Operation type directive is overriding directives defined in ancestor fields

    Operation type directive is overriding directives defined in ancestor fields

    In the following query, all directives declared in the ancestor fields are lost, only the ones in the leaf fields are parsed correctly from the AST:

    query {
      ancestorField @thisDirectiveIsLost { 
        leafField @thisDirectiveWorksWell
      }
    }
    

    For instance, in this query the @include directive is lost, and title is included when it should not:

    query {
      post(id:1) @include(if: false) { 
        title
      }
    }
    

    Inspecting the code, I found out that in Parser/Parser.php, function parseOperation, the operation type directive is being set into $operation:

    $operation->setDirectives($directives);
    

    However, the $operation already had its own directives, parsed through parseBodyItem. Hence, these are being overridden.

    Merging the 2 sets of directives, instead, it works well:

    $operation->setDirectives(array_merge(
        $directives,
        $operation->getDirectives()
    ));
    

    Is that the right solution? Must directives defined next to the operation type be copied into all ancestor fields? And what about leaf fields, which currently have no problem with their own directives? (This solution seems to fix the bug, but I don't know if I may be creating another one? I can't find any reference in the GraphQL spec to what is the expected behavior for operation type directives...)

    opened by leoloso 5
  • ASTField query, not its subqueries

    ASTField query, not its subqueries

    Hi, thanks for this great repo.

    I suggest we get the $ast itself up to the ResolveInfo, not its subfields ($astFields), since we could want to retrieve its location. Example : in a Resolve callback, of a specific field, we could check if user has sufficient rights to read the field, and if not nullify the value and add an error to the ExecutionContext with Location information inside the message (line, column).

    I did a fork to get this working, but I'd love to hear about you on this.

    Thanks

    opened by keywinf 1
Releases(v1.7.0)
  • v1.7.0(Dec 5, 2019)

    • fix some README badges
    • add a more robust implementation for TypeService::getPropertyValue()
    • fix PhpStorm inspection performance complaints
    • fix a bug that prevented using null as a default value
    • add support for error extensions
    • throw ConfigurationException when trying to add two types with the same name to the schema
    • add a getImplementations() method to AbstractInterfaceType, can be used to properly discover all possible types during introspection
    • run Travis CI on PHP 7.2 and 7.3 too
    • run phpstan static analysis (level 1 only) during Travis CI builds
    • rename the Tests directory to tests for consistency with other projects
    • remove some obsolete documentation
    Source code(tar.gz)
    Source code(zip)
  • v1.4.3.4(Jun 7, 2017)

    Bug Fixes:

    https://github.com/Youshido/GraphQL/issues/148 GraphQL error when providing a default value for variables https://github.com/Youshido/GraphQL/issues/144 GraphQL error when not passing value for optional variables https://github.com/Youshido/GraphQL/issues/147 DateTime/Enum types doesn't show invalid input data

    Source code(tar.gz)
    Source code(zip)
  • v1.4.2.7(Dec 12, 2016)

    Despite being a minor update this version might require you to change a few things in your code.

    1. DateTimeType and DateTimeTzType types are now working properly and parsing your input string into the \DateTime object in PHP. Also you can specify the exact format of the DateType when creating a field in constructor (for examplenew DateTimeType("m/d/Y H:ia")) .
    2. EnumType now can return null values as it is in the official JS library and properly use name and value if they are not equal (e.g. {value: 1, name: "ACTIVE"}
    3. All scalar types consider null values as valid values
    Source code(tar.gz)
    Source code(zip)
  • v1.4(Nov 9, 2016)

    This release doesn't break backward compatibility.

    Completely revamped Processor, which now has the proper behaviour for nested structure including:

    • Fragments on Unions and Interfaces
    • NonNullable in different configurations, e.g. NonNull of List of NonNullable and so on
    • Invalid queries like { user { name { } } } are now throwing proper errors
    • Optional arguments now work correctly (but don't forget to list them with a null value, otherwise you will get an error according to the graphql spec)
    • __typanme on new NonNullType(new ObjectType()) now returns the correct name

    All closed issues are covered with relevant unit tests.

    Important Notice Enum default value has to be set using the value, not the name part.

    So the proper use would be:

    'status' => [
        'type' => new EnumType([
            'name'   => 'Statue',
            'values' => [
                [
                    'name'  => 'ACTIVE',
                    'value' => 1
                ],
                [
                    'name'  => 'DELETED',
                    'value' => 2
                ]
            ]
        ]),
        'defaultValue' => 1
    ],
    
    Source code(tar.gz)
    Source code(zip)
  • v1.1.4.3(Jul 14, 2016)

  • v1.1.4.2(Jul 14, 2016)

  • v1.1(Jun 2, 2016)

    Current version brings relay support, 99% test coverage, improved Schema structure and performance improvement.

    All changes are described in the Changelog-1.1. You can also look at Upgrade-1.1 to see how you can quickly update your current codebase.

    Source code(tar.gz)
    Source code(zip)
  • v1.0.1(May 8, 2016)

    Refactored documentation

    This version has updated documentation and slightly updated examples with InputObjectType. No major changes were introduced in this release.

    Source code(tar.gz)
    Source code(zip)
  • v1.0(May 6, 2016)

  • 0.2(May 2, 2016)

Owner
null
Monorepo of the PoP project, including: a server-side component model in PHP, a GraphQL server, a GraphQL API plugin for WordPress, and a website builder

PoP PoP is a monorepo containing several projects. The GraphQL API for WordPress plugin GraphQL API for WordPress is a forward-looking and powerful Gr

Leonardo Losoviz 265 Jan 7, 2023
Monorepo of the PoP project, including: a server-side component model in PHP, a GraphQL server, a GraphQL API plugin for WordPress, and a website builder

PoP PoP is a monorepo containing several projects. The GraphQL API for WordPress plugin GraphQL API for WordPress is a forward-looking and powerful Gr

Leonardo Losoviz 265 Jan 7, 2023
Syntax to query GraphQL through URL params, which grants a GraphQL API the capability to be cached on the server.

Field Query Syntax to query GraphQL through URL params, which grants a GraphQL API the capability to be cached on the server. Install Via Composer com

PoP 4 Jan 7, 2022
Pure PHP APIs with PostgreSQL database, with HTML, CSS & JS simple frontend

The root of the project is html/index.html The folder needs to be put in htdocs folder to run correctly // this link should open the main page if the

ZaydSK 3 Jul 22, 2022
Open Standards Protocol for global Trade & Finance.

Open Standards Protocol for global Trade & Finance. Mitigate Counter-Party Risk by making your Financial Instruments Interoperable & Liquid Start trial under regulatory sandbox environment. Digital Bond, Invoice Factoring, R3 Corda Bridge on XinFin Blockchain

XinFin (eXchange inFinite) 9 Dec 7, 2022
Test your PHP GraphQL server in style, with Pest!

Pest GraphQL Plugin Test your GraphQL API in style, with Pest! Installation Simply install through Composer! composer require --dev miniaturebase/pest

Minibase 14 Aug 9, 2022
GraPHPinator ⚡ 🌐 ⚡ Easy-to-use & Fast GraphQL server implementation for PHP

Easy-to-use & Fast GraphQL server implementation for modern PHP. Includes features from latest draft, middleware directives and modules with extra functionality.

Infinityloop.dev 34 Dec 14, 2022
A PHP port of GraphQL reference implementation

graphql-php This is a PHP implementation of the GraphQL specification based on the reference implementation in JavaScript. Installation Via composer:

Webonyx 4.4k Jan 7, 2023
A PHP implementation of the GraphQL specification based on the JavaScript reference implementation

GraphQL This is a PHP implementation of the GraphQL specification based on the JavaScript reference implementation. Related projects DateTime scalar R

Digia 219 Nov 16, 2022
Create REST and GraphQL APIs, scaffold Jamstack webapps, stream changes in real-time.

API Platform is a next-generation web framework designed to easily create API-first projects without compromising extensibility and flexibility: Desig

API Platform 7.7k Jan 7, 2023
This bundle provides tools to build a complete GraphQL server in your Symfony App.

OverblogGraphQLBundle This Symfony bundle provides integration of GraphQL using webonyx/graphql-php and GraphQL Relay. It also supports: batching with

Webedia - Overblog 720 Dec 25, 2022
GraphQL Bundle for Symfony 2.

Symfony 2 GraphQl Bundle Use Facebook GraphQL with Symfony 2. This library port laravel-graphql. It is based on the PHP implementation here. Installat

Sergey Varibrus 35 Nov 17, 2022
Laravel wrapper for Facebook's GraphQL

Laravel GraphQL Use Facebook's GraphQL with Laravel 6.0+. It is based on the PHP port of GraphQL reference implementation. You can find more informati

Mikk Mihkel Nurges 1.9k Dec 31, 2022
A framework for serving GraphQL from Laravel

Lighthouse A framework for serving GraphQL from Laravel Lighthouse is a GraphQL framework that integrates with your Laravel application. It takes the

NuWave Commerce 3.1k Jan 6, 2023
EXPERIMENTAL plugin extending WPGraphQL to support querying (Gutenberg) Blocks as data, using Server Side Block registries to map Blocks to the GraphQL Schema.

WPGraphQL Block Editor This is an experimental plugin to work toward compatiblity between the WordPress Gutenberg Block Editor and WPGraphQL, based on

WPGraphQL 29 Nov 18, 2022
🍞🧑‍🍳 An on-the-fly GraphQL Schema generator from Eloquent models for Laravel.

An on-the-fly GraphQL Schema generator from Eloquent models for Laravel. Installation Quickstart Model schemas Installation This package requires PHP

Scrn 100 Oct 16, 2022
GraphQL implementation with power of Laravel

Laravel GraphQL Use Facebook GraphQL with Laravel 5.2 >=. It is based on the PHP implementation here. You can find more information about GraphQL in t

Studionet 56 Mar 9, 2022
Add Price Including tax for Magento's "cart" GraphQl query

Comwrap_GraphQlCartPrices Add Price Including tax for Magento's "cart" GraphQl query Query will looks like following: items { id __typenam

Comwrap 1 Dec 2, 2021
A Statamic Pro addon that provides alternative GraphQL queries for collections, entries and global sets.

Statamic Enhanced GraphQL A Statamic CMS GraphQL Addon that provides alternative GraphQL queries for collections, entries and global sets. ⚠️ This is

Grischa Erbe 2 Dec 7, 2021