Open brunobg opened 4 years ago
What you are trying to do is quite tricky and requires deeply modifying the schema build process. Check out https://github.com/nuwave/lighthouse/tree/master/src/Schema for an example of how this can be achieved.
This library contains the necessary primitives to handle directives and customize schema building, and i do not think it should do anything more opinionated.
Since you have a type loader for buildschema
, I was expecting the lib would do something for any node kind or a pointer of how to do it. For other people that one day may find themselves here, I solved it by parsing the AST normally, them modifying it using Visitor.
If I may suggest something, it would be interesting to have a callback for enter/leave like Visitor already in buildSchema.
If I may suggest something, it would be interesting to have a callback for enter/leave like Visitor already in buildSchema.
Can you elaborate? What new use cases would this enable/how does it make existing use cases easier?
Of course! The way I implemented the class loader I described above was more or less like this (and I am still struggling to find out how to replace the Type class on the fields):
$ast = \GraphQL\Language\Parser::parse($data);
Visitor::visit($this->ast, [
NodeKind::SCALAR_TYPE_DEFINITION => function ($node) {
// some code to read the directives and map the $node->name->value to my class
return null;
}
]);
$schemaBuilder = new \GraphQL\Utils\BuildSchema(ast);
$schema = $schemaBuilder->buildSchema();
A visitor is what I expected the callback from BuildSchema
to do -- it's not clear to me from the docs what exactly it does, is it only called for types?
Again, the use cases is what is being done in the tests. You build a schema manually in the VariablesTest with the custom class:
but from what I understood from your answer it's hard to do that from a graphql description. Lighthouse seems to reimplement the schema builder completely. If there was a Visitor-like callback for BuildSchema, I could have a callback that could easily customize the creation of the Schema
object, replacing the generic Scalar type for a MyCustomClass().
Thank you for the interest and prompt replies.
I might have understood this incorrectly. But my understanding is that:
Technically this should be probably done by allowing custom typeLoader
for the buildSchema
. But in the meantime I think there is an existing workaround - you can replace the typeLoader
and use it to replace CustomScalarType
with your custom class.
Something along the lines should probably work (didn't check though):
$ast = \GraphQL\Language\Parser::parse($sdl);
$schemaBuilder = new \GraphQL\Utils\BuildSchema($ast);
$schema = $schemaBuilder->buildSchema();
$originalTypeLoader = $schema->getConfig()->typeLoader;
$schema->getConfig()->typeLoader = function($typeName) use ($originalTypeLoader) {
$type = $originalTypeLoader($typeName);
if ($type instanceof \GraphQL\Type\Definition\CustomScalarType) {
$directives = $type->astNode->directives;
// Look for your `@scalar` directive in $directives and return your custom class instead
$customClass = findMyScalarClassName($directives);
return new $customClass($type->config);
}
return $type;
}
Obviously just a workaround but it should work?
Thank you very much, it seems that will work perfectly.
Ahn, @vladar, sorry for the endless pestering, but besides the bug from #683, it seems that this does not replace types in fields. For example:
scalar TestText @scalar(class: "ModelariumTests\\Laravel\\ScalarTestText")
type User {
id: ID!
name: String!
description: TestText!
}
calls your typeLoader
only with a ObjectType
, and when I traverse the AST the FieldDefinition
$type
is still CustomScalarType
.
Perhaps I'm missing something and it only lazy loads?
Could I use a SchemaExtender
to do this, overriding extendCustomScalarType
?
Good catch. typeLoader
is only partially useful indeed.
Reopening since I think this is a typical use-case (providing custom scalar type class) so we should provide a way to do it in this library.
It took a heck of a lot of work getting here, but I managed to solve this using a $typeConfigDecorator:
use GraphQL\Language\AST\ScalarTypeDefinitionNode;
use GraphQL\Language\AST\TypeDefinitionNode;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
class ScalarDirectiveDecorator
{
private ?ContainerInterface $container;
private LoggerInterface $logger;
public function __construct(?ContainerInterface $container = null, LoggerInterface $logger = null)
{
$this->container = $container;
$this->logger = $logger ?? new NullLogger();
}
/**
* @param array<string, mixed> $config
* @param array<string, mixed> $typeDefinitionMap
*
* @return array<string, mixed>
*/
public function __invoke(array $config, TypeDefinitionNode $typeDefinition, array $typeDefinitionMap): array
{
// Attach the custom scalar handler methods defined in the @scalar(class) directive
if ($typeDefinition instanceof ScalarTypeDefinitionNode) {
$this->logger->debug('Inspecting scalar ' . $config['name']);
foreach ($this->readDirectives($typeDefinition) as $directive => $args) {
if ($directive === 'scalar' && isset($args['class'])) {
$this->logger->debug('Attaching custom scalar class ' . $args['class']);
$scalarInstance = $this->container
? $this->container->get($args['class'])
: new $args['class'];
$config['serialize'] = [$scalarInstance, 'serialize'];
$config['parseValue'] = [$scalarInstance, 'parseValue'];
$config['parseLiteral'] = [$scalarInstance, 'parseLiteral'];
}
}
}
return $config;
}
/**
* @return array<string, array<string, mixed>>
*/
private function readDirectives(ScalarTypeDefinitionNode $node): array
{
$directives = [];
foreach ($node->directives as $directive) {
$args = [];
foreach ($directive->arguments as $arg) {
$args[$arg->name->value] = $arg->value->value;
}
$directives[$directive->name->value] = $args;
}
return $directives;
}
}
You'll need this in your schema:
# used to specify the desired class to execute for a custom scalar
directive @scalar(class: String!) on SCALAR
# add the directive to each custom scalar
scalar Email @scalar(class: "Compwright\\GraphqlScalars\\Email")
Then when loading the schema:
BuildSchema::build($ast, new ScalarDirectiveDecorator());
If multiple decorators are needed to implement various other things, they could be pipelined together using something like https://github.com/thephpleague/pipeline.
An official way to do this without relying on directives and type decorators would be great!
P.S. this solution is included in https://github.com/compwright/graphql-php-scalars.
How do I resolve extended ScalarTypes? https://webonyx.github.io/graphql-php/type-system/scalar-types/ explains how to create a class for them and the tests show how to ]build a Schema manually with them https://github.com/webonyx/graphql-php/blob/1f0ec2408010e01879b1f980afac65de2344c631/tests/Executor/VariablesTest.php#L132, but I can't find how to load them with a parser.
Concrete example, trying to parse this GraphQL:
The idea is to use the directive to resolve the class. I'm parsing it like this:
extendDatatypes
is called forTestType
, but I don't see how to load the scalar classes dynamically. Any hints please?