5

I'm totally new to GraphQL and wanted to play arouund with graphql-php in order to build a simple API to get started. I'm currently reading the docs and trying out the examples, but I'm stuck quite at the beginning.

I want my schema to be stored in a schema.graphql file instead of building it manually, so I followed the docs on how to do that and it is indeed working:

<?php
// graph-ql is installed via composer
require('../vendor/autoload.php');

use GraphQL\Language\Parser;
use GraphQL\Utils\BuildSchema;
use GraphQL\Utils\AST;
use GraphQL\GraphQL;

try {
    $cacheFilename = 'cached_schema.php';
    // caching, as recommended in the docs, is disabled for testing
    // if (!file_exists($cacheFilename)) {
        $document = Parser::parse(file_get_contents('./schema.graphql'));
        file_put_contents($cacheFilename, "<?php\nreturn " . var_export(AST::toArray($document), true) . ';');
    /*} else {
        $document = AST::fromArray(require $cacheFilename); // fromArray() is a lazy operation as well
    }*/

    $typeConfigDecorator = function($typeConfig, $typeDefinitionNode) {
        // In the docs, this function is just empty, but I needed to return the $typeConfig, otherwise I got an error
        return $typeConfig;
    };
    $schema = BuildSchema::build($document, $typeConfigDecorator);

    $context = (object)array();

    // this has been taken from one of the examples provided in the repo
    $rawInput = file_get_contents('php://input');
    $input = json_decode($rawInput, true);
    $query = $input['query'];
    $variableValues = isset($input['variables']) ? $input['variables'] : null;
    $rootValue = ['prefix' => 'You said: '];
    $result = GraphQL::executeQuery($schema, $query, $rootValue, $context, $variableValues);
    $output = $result->toArray();
} catch (\Exception $e) {
    $output = [
        'error' => [
            'message' => $e->getMessage()
        ]
    ];
}
header('Content-Type: application/json; charset=UTF-8');
echo json_encode($output);

This is what my schema.graphql file looks like:

schema {
    query: Query    
}

type Query {
    products: [Product!]!
}

type Product {
    id: ID!,
    type: ProductType
}

enum ProductType {
    HDRI,
    SEMISPHERICAL_HDRI,
    SOUND
}

I can query it for example with

query {
  __schema {types{name}}
}

and this will return the metadata as expected. But of course now I want to query for actual product data and get that from a database, and for that I'd need to define a resolver function.

The docs at http://webonyx.github.io/graphql-php/type-system/type-language/ state: "By default, such schema is created without any resolvers. We have to rely on default field resolver and root value in order to execute a query against this schema." - but there is no example for doing this.

How can I add resolver functions for each of the types/fields?

Constantin Groß
  • 10,719
  • 4
  • 24
  • 50
  • Have you found any answer to this by your own? Care to share here if so? Thanks! – Seb Aug 22 '18 at 18:53
  • Hi @Seb, I posted an answer below. – Constantin Groß Aug 24 '18 at 08:14
  • I found a different way, it feels rather hackish too but it works without creating a Server and will add it as answer for posterity (?) – Seb Aug 27 '18 at 14:24
  • This has become more important to my app, now that there are graphql webpack loaders coming out. I'm duplicating a lot of effort defining my schema as a bunch of big PHP arrays, then building my queries client-side from big strings. I could have `.graphql` files that are loaded in `buildSchema` server-side, and `import` in the client, thus making sure the definitions stay in sync. – Josh from Qaribou Nov 02 '18 at 18:07
  • I found the siler lib has a nice way to load graphql (load the schema, then load the resolvers), but it's simply on top of graphql-php. You can check out their code to see how it's done -- looks like they use `GraphQL\Executor\Executor` a lot to set up the resolvers. https://github.com/leocavalcante/siler/blob/master/src/Graphql/Graphql.php – Josh from Qaribou Nov 02 '18 at 18:11

4 Answers4

2

This approach works without instantiating a Server. In my case, I already have a server and can read HTTP data, all I needed was to read the GraphQL schema and run the query. First I read the schema from a file:

        $schemaContent = // file_get_contents or whatever works for you

        $schemaDocument = GraphQL\Language\Parser::parse($schemaContent);
        $schemaBuilder = new GraphQL\Utils\BuildSchema($schemaDocument);
        $schema = $schemaBuilder->buildSchema();

Then I execute the query passing a custom field resolver:

        $fieldResolver = function() {
            return call_user_func_array([$this, 'defaultFieldResolver'], func_get_args());
        };

        $result = GraphQL\GraphQL::executeQuery(
            $schema,
            $query,        // this was grabbed from the HTTP post data
            null,
            $appContext,   // custom context
            $variables,    // this was grabbed from the HTTP post data
            null,
            $fieldResolver // HERE, custom field resolver
        );

The field resolver looks like this:

private static function defaultFieldResolver(
    $source,
    $args,
    $context,
    \GraphQL\Type\Definition\ResolveInfo $info
) {
    $fieldName = $info->fieldName;
    $parentType = $info->parentType->name;

    if ($source === NULL) {
        // this is the root value, return value depending on $fieldName
        // ...
    } else {
        // Depending on field type ($parentType), I call different field resolvers.
        // Since our system is big, we implemented a bootstrapping mechanism
        // so modules can register field resolvers in this class depending on field type
        // ...

        // If no field resolver was defined for this $parentType,
        // we just rely on the default field resolver provided by graphql-php (copy/paste).
        $fieldName = $info->fieldName;
        $property = null;

        if (is_array($source) || $source instanceof \ArrayAccess) {
            if (isset($source[$fieldName])) {
                $property = $source[$fieldName];
            }
        } else if (is_object($source)) {
            if (isset($source->{$fieldName})) {
                $property = $source->{$fieldName};
            }
        }

        return $property instanceof \Closure
            ? $property($source, $args, $context)
            : $property;
    }
}
Seb
  • 24,920
  • 5
  • 67
  • 85
1

Here's what I ended up doing...

$rootResolver = array(
    'emptyCart' => function($root, $args, $context, $info) {
        global $rootResolver;
        initSession();
        $_SESSION['CART']->clear();
        return $rootResolver['getCart']($root, $args, $context, $info);
    },
    'addCartProduct' => function($root, $args, $context, $info) {
        global $rootResolver;

        ...

        return $rootResolver['getCart']($root, $args, $context, $info);
    },
    'removeCartProduct' => function($root, $args, $context, $info) {
        global $rootResolver;

        ...

        return $rootResolver['getCart']($root, $args, $context, $info);
    },
    'getCart' => function($root, $args, $context, $info) {
        initSession();
        return array(
            'count' => $_SESSION['CART']->quantity(),
            'total' => $_SESSION['CART']->total(),
            'products' => $_SESSION['CART']->getProductData()
        );
    },

and then in the config

$config = ServerConfig::create()
    ->setSchema($schema)
    ->setRootValue($rootResolver)
    ->setContext($context)
    ->setDebug(DEBUG_MODE)
    ->setQueryBatching(true)
;

$server = new StandardServer($config);

It feels rather hack-ish to me, and I should probably outsource the resolvers into separate files, but it works... Still baffled that there are no simple examples for this task, maybe in an even better way than my solution...

Constantin Groß
  • 10,719
  • 4
  • 24
  • 50
0

I'm using root value for this:

<?php

require("vendor/autoload.php") ;
require("exemplo-graphql.php");
require("Usuario.php");

use GraphQL\GraphQL;
use GraphQL\Type\Schema;
use GraphQL\Utils\BuildSchema;

$query = $_REQUEST['query'];

$typeConfigDecorator = function($typeConfig, $typeDefinitionNode) {
    $name = $typeConfig['name'];
    // ... add missing options to $typeConfig based on type $name
    return $typeConfig;
};

$contents = file_get_contents('schema.graphql');
$schema = BuildSchema::build($contents, $typeConfigDecorator);

// $rawInput = file_get_contents('php://input');
$input = json_decode($query, true);
$query = $input['query'];
$variableValues = isset($input['variables']) ? $input['variables'] : null;

try {
    // $rootValue = ['prefix' => 'You said: '];
    $rootValue = [
        'usuario' => function($root, $args, $context, $info) {
            $usuario = new Usuario();
            $usuario->setNome("aqui tem um teste");
            $usuario->setEmail("aqui tem um email");
            return $usuario;
        },
        'echo' => function($root, $args, $context, $info) {
            return "aqui tem um echooo";
        },
        'adicionarUsuario' => function ($root, $args, $context, $info) {
            $usuario = new Usuario();
            $usuario->setNome("aqui tem um teste");
            $usuario->setEmail("aqui tem um email");
            return $usuario;
        }
    ];

    $result = GraphQL::executeQuery($schema, $query, $rootValue, null,
        $variableValues);

    if ($result->errors) {
        $output = [
            'errors' => [
                [
                    'message' => $result->errors
                ]
            ]
    ];
    } else {
        $output = $result->toArray();
    }
} catch (\Exception $e) {
    $output = [
        'errors' => [
            [
                'message' => $e->getMessage()
            ]
        ]
    ];
} 

header('Content-Type: application/json');
echo json_encode($output);
0

By default, schema which was created by using BuildSchema::build() was created without any resolvers. So we need to define our custom resolvers as follows:

$contents = file_get_contents($this->projectDir.'/config/schema.graphql');
$typeConfigDecorator = function($typeConfig, $typeDefinitionNode) {
    $name = $typeConfig['name'];
    if ($name === 'Query') {
       $typeConfig['resolveField'] =
           function ($source, $args, $context, ResolveInfo $info) {
               if ($info->fieldDefinition->name == 'login') {
                   if ($args['userName'] === 'test' && $args['password'] === '1234') {
                       return "Valid User.";
                   } else {
                       return "Invalid User";
                   }
               } elseif ($info->fieldDefinition->name == 'validateUser') {
                   if ($args['age'] < 18) {
                       return ['userId' => $args['userId'], 'category' => 'Not eligible for voting'];
                    } 
                    }
                }
                }
            ;
        }
        return $typeConfig;
    };
$schema = BuildSchema::build($contents, $typeConfigDecorator);

The above example I have added resolvers for my two queries namely 'login' and 'validateUser.'

No need to define any root values and defaultFieldResolver. Our custom resolvers are enough.