Giter Club home page Giter Club logo

graphql's Introduction

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

graphql's People

Contributors

armetiz avatar bartfeenstra avatar bgfist avatar big-shark avatar danez avatar dezzak avatar fritz-gerneth avatar fubhy avatar gitter-badger avatar herlevsen avatar jalle19 avatar keichinger avatar lslinnet avatar m-naw avatar mathroc avatar mrbarletta avatar oligus avatar philsturgeon avatar pmelab avatar portey avatar powerkiki avatar roippi avatar scottjoplin avatar seb2411 avatar stefanorg avatar symm avatar theghoul21 avatar viniychuk avatar xuxucode avatar zombaya avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

graphql's Issues

Fragment reference "fReservation" not found on model "CourtReservation"

Full query:

query {
    user {
        ...fUser
        reservations {
            ...fReservation
        }
    }
}
fragment fReservation on Reservation {
    id
    ... on CourtReservation {
        players {
            id
            user {
                ...fUser
            }
        }
    }
    ... on ClassReservation {
        user {
            ...fUser
        }
    }
}
fragment fUser on User {
    id
    fullname
}

When I debug ResolveValidator::assertValidFragmentForField(), $fragment->getModel() seems to be "User".

Where does User come from..? The fragment is only applied to reservations, which is a new ListType(new ReservationInterface()). Is it confused by fUser?

When I try another server library, the query works. Which is wrong? The query seems correct...

NonNullType inside a List of InputObjects are able to be null

As the title says, if you define a field inside an inputobject that is in a list to be NonNullable, the validator does not catch it if the variable is not set.

I've attached my test-classes: TestMutation.zip.

The query I tried to run:

mutation{
  Test(test:{
    a:[
    ],
    b:[
      {
        c:"aaaa"
      },
      {

      }
    ],
  }) {
    name
  }
}

The rules for TestInput:

$config -> addField('a' , ['type' => new ListType(new IntType())        , 'description' => 'a'])
        -> addField('b' , ['type' => new ListType(new InputObjectType([
                                       'name'    => 'test_b',
                                       'fields'  => [
                                         'c'           =>  new NonNullType(new StringType()),
                                         ]
                                         ]))
                                      ])
        -> addField('d' , ['type' => new InputObjectType([
                                       'name'    => 'test_d',
                                       'fields'  => [
                                         'c'           =>  new NonNullType(new StringType()),
                                         ]
                                         ])
                                      ])

The validator does catch it if the field c of d is not set.

Extending AbstractField does not work for mutation fields

Hi @viniychuk,

Maybe, I am doing something wrong, but when I try define mutations in such a way:

private function getRootMutation() {
        return new ObjectType([
            'name' => 'RootMutation',
            'fields' => [
                // here's our likePost mutation
                'likePost' => new LikePost()
            ]
        ]);
    }

and then define LikePost in the following way:

class LikePost extends AbstractField
{

    public function build(FieldConfig $config) {

        $config->addArgument('id', new NonNullType(new IntType()));
    }

    public function getType()
    {
        return new IntType();
    }

    public function resolve($value, array $args, ResolveInfo $info)
    {
        return 2;
    }
}

then it does not work. I receive error that likePost field is undefined.
I tried also extending AbstractInputField, however InputFieldConfig does not contain addArgument function.

Is it possible to do in the current version?

Bug: invalid output type returned without error if NonNull type used

If !Int is resolved to null, there's no error or exception thrown

$config->addField('id', [
  'type' => new NonNullType(new IntType()),
  'resolve' => function() { return null; }
]);

graphql-js is notificating about this behavior with

{
      "message": "Cannot return null for non-nullable field User.id.",
      "locations": [
        {
          "line": 5,
          "column": 5
        }
      ]
    }

in response errors key. Same is returned if resolver returns string not coercible to int.

NonNullType not casting to string in query

I'm using NonNullType in my return type like this:

class PersonType extends AbstractObjectType
{
    /**
     * @param ObjectTypeConfig $config
     *
     * @return mixed
     */
    public function build($config)
    {
        $config->addFields([
            'id' => new NonNullType(new IdType()),
            'firstName' => new NonNullType(new StringType()),
            'lastName' => new NonNullType(new StringType()),
        ]);
    }
}

The IDs of my persons are UUIDs. With the code like this, the result is:

{
    "data": {
        "persons": [
            {
                "id": {},
                "firstName": "Bernhard",
                "lastName": "Schussek"
            }
        ]
    }
}

As you can see, the UUID is not cast to a string, but converted to {}. However if I remove the NonNullType around IdType, the result is as expected:

{
    "data": {
        "persons": [
            {
                "id": "6cfb044c-9c0a-4ddd-9ef8-a0b940818db3",
                "firstName": "Bernhard",
                "lastName": "Schussek"
            }
        ]
    }
}

Definition of args in object oriented approach.

GraphQL Fields can have arguments. It is easy to do in inline-approach, however, I cannot achieve it in object oriented approach. I need it badly. I edited this function in the following way:

public function getRules()
    {
        return [
            'name'        => ['type' => TypeService::TYPE_STRING, 'required' => true],
            'description' => ['type' => TypeService::TYPE_STRING],
            'fields'      => ['type' => TypeService::TYPE_ARRAY_OF_FIELDS_CONFIG, 'final' => true],
            'interfaces'  => ['type' => TypeService::TYPE_ARRAY_OF_INTERFACES],
            'args'     => ['type' => TypeService::TYPE_ARRAY_OF_INPUT_FIELDS]
        ];
    }

However, still $args are empty in the resolve function. Do you know how to do that easily? I will be grateful for any help.

Is it possible to extend Types?

Is there a possibility of extending types?
For example, if I have PostType and I want to create the next type called SpecialPostType (which has all fields that PostType has, but also a few additional), can I extend it somehow?

I do not want to use Interfaces, because as far as I understand, Interfaces will make me to define resolve functions for PostType and SpecialPostType separately.

Query with missing fragment does not throw exception

if fragment does not exist, PHP Fatal error: Call to a member function getModel() on null.

query {
  posts {
    ...post
  }
}

I believe throwing of exception with something like Unknown fragment "post". will be more usefull

thoughts on implementing Query Complexity Analysis?

This is a feature that's popping up in a few other framework implementations, and I think it's pretty killer - it lets you do a reasonable job at statically detecting pathologic queries.

There are some more complex ideas floating out there in the community, namely graphql/graphql-js#304. This could be used both for complexity/cost analysis in addition to query resolution optimization, though it seems more useful for optimization. Probably overkill here.

Has anyone already thought of implementing this here, or should I take a crack at it?

Support fragments on Unions and Interfaces

@fubhy's comments in gitter:

Fragments currently don't work with interfaces or union types... only normal types or composite types.

E.g. this doesn't work:

query {
  somethingThatReturnsAUnion(foo: "bar") {
     ... on Something { // Something is a valid type for this union!
        id
     }
  }
}

That is, because we currently only load the inner type (resolve any compose types) and then compare the name of that type with the name of the fragment. In case of unions/interfaces however, we need to also compare it with any of the possible types of the interface/union.
And even possible further nested types.

Will add failing test case.

variables in nested fields and queries don't work

This works fine:

{ foo { bar(value:"baz") } }

But the following equivalent query with provided variables {value:"baz"} bombs out:

query nestedQuery($value:String!){
  foo {
    bar(value:$value)
}

Error message is somewhat opaque: "Value not set to variable else"

I've provided a regression test here: roippi@54f310b

The fix is non-trivial so I don't know if/when I'll have a pull request available. I've traced through it and it looks like $this->resolveValidator->validateArguments is only getting invoked at the operation level, not at the individual field level.

Thanks for the awesome library.

when we can not resolve field, it passes null as $contextValue for resolve function

Hi

class PostType extends \Youshido\GraphQL\Type\Object\AbstractObjectType
{

    /**
     * @param \Youshido\GraphQL\Config\Object\ObjectTypeConfig|TypeConfigInterface $config
     * @return mixed
     */
    public function build($config)
    {
        $config->addField('id', [
            'type' => new IdType(),
            'resolve' => function ($value, $args){
                return $value['post_id'];
            },
        ]);

in my db, the id field is named post_id, i want to resolve post_id to id
but in resolve function, $value is always null

it seems you have issue in https://github.com/Youshido/GraphQL/blob/master/src/Execution/Processor.php#L274

because

        if (!$resolved && $field->getType()->getNamedType()->getKind() == TypeMap::KIND_SCALAR) {
            $resolved = true;
        }
        if ($resolveFunction = $field->getConfig()->getResolveFunction()) {
           .....
            } else {
                $resolverValue = $resolveFunction($resolved ? $resolverValue : $contextValue, $fieldAst->getKeyValueArguments(), $resolveInfo);
            }
        }

when $resolved = true and $resolverValue = null, $resolveFunction will have null $contextValue

Unexpected input field value causes `TypeError`

Hi. I have this code in the userEdit field:

/**
 * @inheritdoc
 */
public function getName()
{
    return 'userEdit';
}
/**
 * @inheritdoc
 */
public function getType()
{
    return new UserType();
}
/**
 * @inheritdoc
 */
public function build(FieldConfig $config)
{
    $this->addArgument(
        new InputField([
            'name' => 'id',
            'type' => new NonNullType(new IntType()),
        ])
    )->addArgument(
        new InputField([
            'name' => 'data',
            'type' => new NonNullType(new InputObjectType([
                'name' => 'data',
                'fields' => [
                    [
                        'name' => 'fullName',
                        'type' => new NonNullType(new StringType()),
                    ],
                ],
            ])),
        ])
    );
    parent::build($config);
}

and the command:

mutation m {
  userEdit(id: 1000000, data: 0) {
    fullName
  }
}

returns:

{
  "errors": {
    "message": "Type error: Argument 1 passed to Youshido\\GraphQL\\Validator\\ResolveValidator\\ResolveValidator::processInputObject() must be an instance of Youshido\\GraphQL\\Parser\\Ast\\ArgumentValue\\InputObject, instance of Youshido\\GraphQL\\Parser\\Ast\\ArgumentValue\\Literal given, called in /var/www/symfony/vendor/youshido/graphql/src/Validator/ResolveValidator/ResolveValidator.php on line 101"
  }
}

Any unexpected value ([], 0, "str") for the data argument is not validated and causes to TypeError.

Also this request for the same schema:

mutation m {
  userEdit(id: 1000000, data: {unknownField: "a"}) {
    fullName
  }
}

returns only [] without any errors/exceptions.

It is possible to execute invalid query

Query like this:

{
  user(id: 11457349) {

  } 
}

is invalid, but it is possible to execute it with package. It will resolve to all user keys, but I don't believe this is valid either. Can you confirm this? Or am I mistaken?

Minor documentation issue?

Hi,

First up - awesome work, I just started looking into this project after working with GraphQL-php for some time. I really like the OOP type declaration approach.

I come across an issue in the getting started document, which I think might just be outdated documentation?

public function build(TypeConfigInterface $config)
{
    $config
        ->addField('title', new NonNullType(new StringType()), [
            'description'       => 'This field contains a post title',
            'isDeprecated'      => true,
            'deprecationReason' => 'field title is now deprecated',
            'args'              => [
                'truncated' => new BooleanType()
            ],
            'resolve'           => function ($value, $args) {
                return (!empty($args['truncated'])) ? explode(' ', $value)[0] . '...' : $value;
            }
        ])
        ->addField('summary', new StringType())
        ->addField('likeCount', new IntType());
    $config->addArgument('id', new IntType());
}

public function resolve($value, $args, $info)
{
    return [
        "title"     => "Title for the latest Post",
        "summary"   => "Post summary",
        "likeCount" => 2
    ];
}
  1. inline documentation says it is ObjectTypeConfig and not TypeConfigInterface.

  2. addFields only takes 2 arguments, how is it configured then?

  3. AbstractObjectType does'nt not contain a resolve() to override, and if I define a resolve function here, it is not called.

How to manage a List of Posts

In the examples ListType is used with interfaces
But what if I want a list of posts? How do you handle that?

Tried as ListType ( new PostType()) and returned an array of posts and it didn't work.

Thanks!

PS: Thanks for the awesome work ! tried many tools and this one was the best php helper/library I found, very well documented. I just made a PR to the awesome list for PHP related projects:
chentsulin/awesome-graphql#71 #71

How to create a Mutation with a List of (something) as argument

Lets say I have a mutation like this

mutation
{createPost(
  title: 'Copa America', 
  description: ' Latest results ',
  authors: [  
     {name: 'Maradona', email: '[email protected]' } ,
     {name: 'Messi', email: '[email protected]' } ]  
)}

How Do I handle this scenario? -

I tried the author ListType( new AuthorType()) ?

ListType is not iterating each AuthorType sent on the request so its failing with Not valid Type....

ResolveValidator line

if (!$argumentType->isValidValue($argumentType->parseValue($argument->getValue()->getValue()))) {

Is receiving the entire array sent and not a single iteration..

I guess we need a InputListType ? or I am missing something

Requesting undefined subfield results in a fatal error

If you request a subfield which is not defined, you get a fatal error.

Example-query

{Test{
  name,
  bla {
  }
}}

The test-type only has the field name defined. The subfield bla is never defined which causes the fatal error.

Testfiles

testfiles.zip

Stacktrace

( ! ) Fatal error: Uncaught TypeError: Argument 2 passed to Youshido\GraphQL\Execution\Processor::processQueryAST() must be an instance of Youshido\GraphQL\Field\AbstractField, null given, called in /home/pieter/workfiles/sigura-industries/vendor/youshido/graphql/src/Execution/Processor.php on line 332 and defined in /home/pieter/workfiles/sigura-industries/vendor/youshido/graphql/src/Execution/Processor.php on line 127
( ! ) TypeError: Argument 2 passed to Youshido\GraphQL\Execution\Processor::processQueryAST() must be an instance of Youshido\GraphQL\Field\AbstractField, null given, called in /home/pieter/workfiles/sigura-industries/vendor/youshido/graphql/src/Execution/Processor.php on line 332 in /home/pieter/workfiles/sigura-industries/vendor/youshido/graphql/src/Execution/Processor.php on line 127
Call Stack
#TimeMemoryFunctionLocation
10.0002384928{main}( ).../graphql.php:0
20.0089706880Sigura\Mods\Industries\Graphql->__construct( ).../graphql.php:8
30.12322184024Youshido\GraphQL\Execution\Processor->processPayload( ).../Graphql.class.php:122
40.12482253088Youshido\GraphQL\Execution\Processor->executeOperation( ).../Processor.php:76
50.12492270560Youshido\GraphQL\Execution\Processor->processQueryAST( ).../Processor.php:118
60.12512395400Youshido\GraphQL\Execution\Processor->collectValueForQueryWithType( ).../Processor.php:135
70.12522398864Youshido\GraphQL\Execution\Processor->processQueryFields( ).../Processor.php:171
80.12542424840Youshido\GraphQL\Execution\Processor->processQueryAST( ).../Processor.php:332

Non-null field can return null in response

tried on last version

schema:

type Query {
  user: User
} 
type User {
  key: Int!
}

query:

{
  user {
    key 
  }
}

If i user graphql-js:

{
  "data": {
    "user": null
  },
  "errors": [
    {
      "message": "Cannot return null for non-nullable field User.key.",
      "locations": [
        {
          "line": 3,
          "column": 5
        }
      ]
    }
  ]
}

With this package:

{
  "data": {
    "user": {
      "key": null
    }
  }
}

There is no error thrown

Confusing of the naming

I think the name Field used for GraphQL(Youshido) is different from Field in GraphQL documentation.

Field in GraphQL(Youshido) refers to an instance of a type(object) (including the resolve method).
Field in GraphQL(doc) refers to property of a object.

If my understanding is correct. Shall we use another name for Field for Youshido?

Mutation helper class - how to

Hi,

I have a Yii project that i have implemented with Youshido/GraphQL and it works great!

Based on Yiis classes i can autogenerate code for querys and now im looking on mutation.

Smal repo: https://github.com/Vendium/Youshido-GraphQL-Yii

In your documentation you have written that "You can create a mutation by extending AbstractObjectType", my question is how to do this so you dont get a big Schema file. For query its made easy by working with types that have relations with each other.

So dose any one have a small code exampel of how to work with mutations without geting a big schema file and what i assume is to place mutations in type files.

thanks!

Strange issue with list of values

Hi
Query:

mutation {
  upvote(idPhotos:[320131273]) {
    id
  }
}

If i define idPhotos this way

$config->addArgument('idPhotos', new NonNullType(new ListType(new IntType())));

I get error Not valid type for argument "idPhotos" in query "upvote", but if I define it this way

$config->addArgument('idPhotos', new NonNullType(new ListType(new NonNullType(new IntType()))));

everything works as expected.

DateTimeType clarification

Hi,
using DateTimeType enforce string validation against a date format, i found that the actual implementation of DateTimeType doesn't allow null value as mutation parameter and as scalar value.

As an example if in the todo example app we add a expire field as DateTime we can query

class TodoType extends AbstractObjectType
{
    public function build($config)
    {
        $config->addFields([
            'id' => new NonNullType(new IdType()),
            'title' => new StringType(),
            'completed' => new BooleanType(),
            'expire' => new DateTimeType("d-m-Y")
        ]);
    }

}

if we issue a query

fragment todo on Todo {
  id
  title
  completed
  expire
}

{
  todos {
    ...todo
  }
}

we get

{
  "data": {
    "todos": [
      null
    ]
  },
  "errors": [
    {
      "message": "Not valid resolved type for field \"expire\""
    }
  ]
}

Also in case of mutation

mutation {
   save(id:1, title: "todo mod", expire: null) {
    ...todo
  }
}

the error is

{
  "data": {
    "save": null,
    "toggleAll": [
      null
    ]
  },
  "errors": [
    {
      "message": "Not valid type for argument \"expire\" in query \"save\"",
      "locations": [
        {
          "line": 10,
          "column": 34
        }
      ]
    },
    {
      "message": "Not valid resolved type for field \"expire\""
    }
  ]
}

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
          },
          ....
        ]
      }
    }
  }
}

AbstractInputObjectType: Unknown argument 'id' on field 'createPart' when performing a mutation

Hi!

Thanks for the hard work you have put into this project!

I have a mutation field:

class CreatePartField extends AbstractField
{
  public function getType()
  {
      return new PartInputType();
  }

  public function build(FieldConfig $config)
  {
      $config->addArguments(new PartInputType());
  }

  public function resolve($value, array $args, ResolveInfo $info)
  {
     // return something...
  }
}

And the type:

class PartInputType extends AbstractInputObjectType
{
    public function build($config)
    {
        $config
            ->addField('id', new IntType())
            ->addField('name', new NonNullType(new StringType()))
            ->addField('description', new NonNullType(new StringType()))
            ->addField('notes', new StringType())
            ->addField('minimumStock', new IntType());
    }
}

I've add the field to the mutation fields:

class GraphQLSchema extends AbstractSchema
{
    public function build(SchemaConfig $config)
    {
        $config->getQuery()->addFields([
            new RetrievePartField(),
            new AllPartsField(),
        ]);

          $config->getMutation()->addFields([
          new CreatePartField()
        ]);
    }

}

I send the mutation request:

mutation { createPart( id: 3, name: "Another test",  description: "Another  description", minimumStock: "12", notes: "Another note")  }

I keep getting the error: Unknown argument "id" on field "createPart"

The field doesn't seem to like accepting AbstractInputObjectType as an argument (maybe?)

I've looked into all of the docs and examples, but I have not yet found a solution...

Perhaps I'm formatting the mutation request wrong...

Any help would be greatly appreciated! Thank you!

Problem with enum

Hi,

Using a "-" in the name of an enum causes this error:

{
  "errors": [
    {
      "message": "Config is not valid for TestType\nField \"values\" expected to be \"type\" but got \"array\""
    }
  ]
}

Code:

use \Youshido\GraphQL\Type\Object\AbstractObjectType;
use \Youshido\GraphQL\Type\Scalar\IdType;
use \Youshido\GraphQL\Type\Scalar\StringType;
use \Youshido\GraphQL\Type\Scalar\DateType;
use \Youshido\GraphQL\Type\Scalar\BooleanType;
use \Youshido\GraphQL\Type\ListType\ListType;
use \Youshido\GraphQL\Type\Enum\AbstractEnumType;

class TestType extends AbstractEnumType
{
    public function getValues()
    {
        $result = array();
        $result[]= array(
                'value' => 1,
                'name' => "aaa-", 
            );
        $result[]= array(
                'value' => 2,
                'name' => "bbb", 
        );
        return $result;
    }


}

Valid query not possible to execute

This query combining unions and fragments does not work:

{
  user(id: 2) {
    ...user
  }
}

fragment user on UserUnion {
  ... on User {
    id
  }
}

schema contains query user which can return UserUnion which will be resolved to type User with field id. Exception thrown: Fragment reference "user" not found on model "User"

Resolve only the requested fields

Is there a recommended way only to resolve the fields that the consumer has requested? In our model we have a few run-time calculated fields, ideally these aren't resolved on every request if the consumer does not actually ask for them.

Poking around the code I can't see a way to find the actual fields requested in the field resolver. I can see the entire query but as a field can be exposed from multiple nodes in the graph it's hard to infer the context it is being resolved for.

Thanks

resolve function on ObjecType not allowed?

Here's my example which the runtime throws the exception:

Call to undefined method Youshido\GraphQL\Type\Object\ObjectType::resolve()

        $productType = new ObjectType([
            'name' => 'ProductType',
            'fields' => [
                'sId' => new IntType(),
                'title' => new StringType(),
                'comments' => new StringType(),
                'price' => new FloatType(),
                'priceStr' => new StringType(),
                'finalPrice' => new FloatType(),
                'finalPriceStr' => new StringType(),
                'currency' => new StringType(),
                'Link' => new StringType(),
                'link' => new StringType(),
            ],
            'resolve' => function()     <------- is this allowed?
            {
                return [
                    'sId' => 100,
                    'title' => 'Starwar film',
                    'comments' => 'no comment',
                    'price' => 13.5,
                    'priceStr' => '$13.5',
                    'finalPrice' => 10.5,
                    'finalPriceStr' => '$10.5',
                    'currency' => 'USD',
                    'Link' => 'http://google.com',
                    'link' => 'http://google.com'
                ];
            }
        ]);

I was following the first example in this section of readMe:
https://github.com/Youshido/GraphQL#objects

Syntax error in graphql not throws error

tried on last version

schema:

type Query {
  user: User
} 
type User {
  key: Int
}

query:

{
  user {
    key { // empty selection, causing invalid query

    }
  }
}

If i user graphql-js:

{
  "errors": [
    {
      "message": "Syntax Error GraphQL request (5:5) Expected Name, found }\n\n4:       \n5:     }\n       ^\n6:   }\n",
      "locations": [
        {
          "line": 5,
          "column": 5
        }
      ]
    }
  ]
}

With this package:

{
  "data": {
    "user": {
      "key": null
    }
  }
}

There is no error thrown and also invalid data, key data should be available only if execution of query started, not in part of validating schema

Optional argument throws if missing in query

schema:

type Query {
  getSomething(sort:SortType): String //sort not required
}

query:

query SomeQuery($sort:SortType) {
  getSomething(sort: $sort)
}

variables:
NONE or

{}

response

{
  "errors": [
    {
      "message": "Variable \"sort\" does not exist for query \"getSomething\""
    }
  ]
}

Error locations

I was testing this library last week and noticed that you do not support the error locations that are part of the GraphQL spec.

Is there a way to enable this or is this in the works?

Query where an object has no fields requested, returns internal representation

Hi, we've run in to a situation where if we execute a query such as:

{
  name
  primaryAddress
}

Where primaryAddress is a complex object with it's own type. We receive a response like so:

{
  "name": "Bob",
  "primaryAddress": {

        },
        "__cloner__": {

        },
        "__isInitialized__": false
      }
}

The types are configured like so:

<?php
namespace App\AppBundle\GraphQL;

use Youshido\GraphQL\Type\ListType\ListType;
use Youshido\GraphQL\Type\Object\AbstractObjectType;
use Youshido\GraphQL\Type\Scalar\BooleanType;
use Youshido\GraphQL\Type\Scalar\DateTimeTzType;
use Youshido\GraphQL\Type\Scalar\DateType;
use Youshido\GraphQL\Type\Scalar\FloatType;
use Youshido\GraphQL\Type\Scalar\IdType;
use Youshido\GraphQL\Type\Scalar\IntType;
use Youshido\GraphQL\Type\Scalar\StringType;

class PersonType extends AbstractObjectType {

    /**
     * @param mixed $config
     */
    public function build($config) {
        return $config
        ->addFields(['name' => ['type' => new StringType(), 'description' => 'The name of the person']])
        ->addFields(['primaryAddress' => ['type' => new AddressType(), 'description' => 'Their primary address']])
    }

    /**
     */
    public function getName() {
        return "Person";
    }
}

class AddressType extends AbstractObjectType {

    /**
     * @param mixed $config
     */
    public function build($config) {
        return $config
                      ->addFields(['street' => ['type' => new StringType(), 'description' => 'The street']])
                      ->addFields(['postcode' => ['type' => new StringType(), 'description' => 'The postcode']]);
    }

    /**
     */
    public function getName() {
        return 'Address';
    }
}

We are using doctrine behind the scenes, this seems to be outputting the contents of our proxy object. If we execute the query with an empty field list (e.g. primaryAddress {}) then the query fails with an expected message of "You have to specify fields for primaryAddress". I would expect the same result if you miss off the braces.

Are we misconfiguring our types or is this a bug?

Regards,

Fragments do not appear to work

{
            oneComparison: Post(id: 1) {
                title
            }
            twoComparison: Post(id: 1) {
                ...Fields
            }
        }
        
        fragment Fields on Post {
            title
        }

Returns this:

{"data":{"oneComparison":{"title":"string"},"twoComparison":[]}}

Invalid sub-selection of field

tried on last version

schema:

type Query {
  user: User
} 
type User {
  key: Int
}

query:

{
  user {
    key { 
      someKey // trying to sub-select on Int
    }
  }
}

If i user graphql-js:

{
  "errors": [
    {
      "message": "Field \"key\" of type \"Int\" must not have a sub selection.",
      "locations": [
        {
          "line": 3,
          "column": 9
        }
      ]
    }
  ]
}

With this package:

{
  "data": {
    "user": {
      "key": null
    }
  }
}

There is no error thrown and also invalid data, key data should be available only if execution of query started, not in part of validating schema

Non-null argument for input object of mutation fails validation

I have a mutation, defined as such:

<?php
namespace AppBundle\GraphQL\Mutation;

use AppBundle\GraphQL\Type\TestMutationInputType;
use Youshido\GraphQL\Config\Field\FieldConfig;
use Youshido\GraphQL\Execution\ResolveInfo;
use Youshido\GraphQL\Type\NonNullType;
use Youshido\GraphQL\Type\Scalar\IntType;
use Youshido\GraphQLBundle\Field\AbstractContainerAwareField;

class TestMutationField extends AbstractContainerAwareField
{

    /**
     * {@inheritdoc}
     */
    public function getType()
    {
        return new IntType();
    }

    /**
     * {@inheritdoc}
     */
    public function build(FieldConfig $config)
    {
        $config->addArgument('input', new NonNullType(new TestMutationInputType()));
    }

    /**
     * {@inheritdoc}
     */
    public function resolve($value, array $args, ResolveInfo $info)
    {
        return 0;
    }
}

The input type is defined as such:

<?php
namespace AppBundle\GraphQL\Input;

use Youshido\GraphQL\Type\InputObject\AbstractInputObjectType;
use Youshido\GraphQL\Type\ListType\ListType;
use Youshido\GraphQL\Type\NonNullType;
use Youshido\GraphQL\Type\Scalar\StringType;

class TestMutationInputType extends AbstractInputObjectType
{

    /**
     * {@inheritdoc}
     */
    public function build($config)
    {
        $config->addFields([
            'nullField' => new StringType(),
            'nonNullField' => new NonNullType(new StringType()),
        ]);
    }
}

Executing this mutation and providing values for all the fields on the input type fails with the following message:

{
  "data": {
    "testMutation": null
  },
  "errors": [
    {
      "message": "Not valid type for argument \"input\" in query \"testMutation\""
    }
  ]
}

If I then remove the NonNullType from nonNullField the mutation executes correctly. I've poked through the code but nothing obvious stood out to me.

Regards

Root query and mutation don't have __typename

Query:

mutation { // or query or empty
  __typename
}

Response:

{
  "errors": [
    {
      "message": "Field \"__typename\" not found in type \"Mutation\"",
      "locations": [
        {
          "line": 20,
          "column": 3
        }
      ]
    }
  ]
}

BTW: Big thanks for locations key 👍

if i use TimestampType with value integer, it will return null after being resolved

$config->addField('date', new TimestampType());

In my database, i have field date as unix timestamp (which is integer)

In this class
https://github.com/Youshido/GraphQL/blob/master/src/Type/Scalar/TimestampType.php

public function isValidValue($value)
    {
        if (is_object($value)) {
            return true;
        }
        return is_int($value);
    }

if i pass an integer, it will return true
but when it is being serialized

 public function serialize($value)
    {
        if ($value === null || !is_object($value)) {
            return null;
        }
        return $value->getTimestamp();
    }

the previous valid integer will be serialized into null

Resolve cannot be a field of ObjectType - mistake in description

Thank you for good documentation, however. there is a problem with description of inline-approach:

<?php
use Youshido\GraphQL\Type\Object\ObjectType;
use Youshido\GraphQL\Type\Scalar\StringType;

// creating a root query structure
$rootQueryType = new ObjectType([
    // name for the root query type doesn't matter, by the convention it's RootQueryType
    'name'   => 'RootQueryType',
    'fields' => [
        'latestPost' => new ObjectType([ // the Post type will be extended from the generic ObjectType
            'name'    => 'Post', // name of our type – "Post"
            'fields'  => [
                'title'   => new StringType(),  // defining the "title" field, type - String
                'summary' => new StringType(),  // defining the "summary" field, also a String type
            ],
            'resolve' => function () {          // this is a resolve function
                return [                        // for now it returns a static array with data
                    "title"   => "New approach in API has been revealed",
                    "summary" => "This post describes a new approach to create and maintain APIs",
                ];
            }
        ])
    ]
]);

Resolve function cannot be there. It is not valid.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.