From e686ca8554619e9264bdd064c87a4531c70b88cf Mon Sep 17 00:00:00 2001 From: Cyril Mizzi Date: Tue, 1 Aug 2017 14:49:16 +0200 Subject: [PATCH 01/11] extends EloquentObjectType to ObjectType --- src/Type/EloquentObjectType.php | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/Type/EloquentObjectType.php diff --git a/src/Type/EloquentObjectType.php b/src/Type/EloquentObjectType.php new file mode 100644 index 0000000..7243f85 --- /dev/null +++ b/src/Type/EloquentObjectType.php @@ -0,0 +1,35 @@ +model = $config['model']; + parent::__construct($config); + } + + /** + * Return the corresponding model + * + * @return Model + */ + public function getModel() { + return $this->model; + } +} From f65e1d8d4efd26ec57a7a27d1a40dc76c75193a7 Mon Sep 17 00:00:00 2001 From: Cyril Mizzi Date: Tue, 1 Aug 2017 14:50:05 +0200 Subject: [PATCH 02/11] creates basic transformers At this time, I don't really know how to manage them --- src/Support/Interfaces/ModelAttributes.php | 23 ++++++ src/Support/Type.php | 61 ---------------- src/Transformer/Transformer.php | 25 +++++++ src/Transformer/TransformerInterface.php | 20 ++++++ src/Transformer/Type/DefaultTransformer.php | 77 +++++++++++++++++++++ src/Transformer/Type/ModelTransformer.php | 74 ++++++++++++++++++++ 6 files changed, 219 insertions(+), 61 deletions(-) create mode 100644 src/Support/Interfaces/ModelAttributes.php create mode 100644 src/Transformer/Transformer.php create mode 100644 src/Transformer/TransformerInterface.php create mode 100644 src/Transformer/Type/DefaultTransformer.php create mode 100644 src/Transformer/Type/ModelTransformer.php diff --git a/src/Support/Interfaces/ModelAttributes.php b/src/Support/Interfaces/ModelAttributes.php new file mode 100644 index 0000000..50fdbbc --- /dev/null +++ b/src/Support/Interfaces/ModelAttributes.php @@ -0,0 +1,23 @@ +getFields(); - $attributes = $this->getAttributes(); - $interfaces = $this->getInterfaces(); - - foreach ($fields as $key => $field) { - if (is_array($field) and method_exists($this, 'getFieldResolver')) { - $resolver = $this->getFieldResolver($key, $field); - - if ($resolver !== null) { - $fields[$key]['resolve'] = $resolver; - } - } - } - - $attributes = array_merge($attributes, [ - 'fields' => $fields, - 'name' => $this->getName(), - 'description' => $this->getDescription(), - ]); - - if (!empty($interfaces)) { - $attributes['interfaces'] = $interfaces; - } - - return $attributes; - } - - /** - * {@inheritDoc} - */ - public function toType() { - return new ObjectType($this->toArray()); - } } diff --git a/src/Transformer/Transformer.php b/src/Transformer/Transformer.php new file mode 100644 index 0000000..ae4fb1b --- /dev/null +++ b/src/Transformer/Transformer.php @@ -0,0 +1,25 @@ +app = $application; + } +} diff --git a/src/Transformer/TransformerInterface.php b/src/Transformer/TransformerInterface.php new file mode 100644 index 0000000..aff7ee3 --- /dev/null +++ b/src/Transformer/TransformerInterface.php @@ -0,0 +1,20 @@ +getFields(); + $attributes = $type->getAttributes(); + $interfaces = $type->getInterfaces(); + + foreach ($fields as $key => $field) { + if (is_array($field)) { + $resolver = $this->getFieldResolver($type, $key, $field); + + if ($resolver !== null) { + $fields[$key]['resolve'] = $resolver; + } + } + } + + $attributes = array_merge($attributes, [ + 'fields' => $fields, + 'name' => $this->getName(), + 'description' => $this->getDescription() + ]); + + if (!empty($nterfaces)) { + $attributes['interfaces'] = $interfaces; + } + + return new ObjectType($attributes); + } + + /** + * Resolve given field + * + * @param TypeInterface $type + * @param string $name + * @param array $field + * + * @return callable|null + */ + private function getFieldResolver(TypeInterface $type, $name, array $field) { + if (array_key_exists('resolve', $field)) { + return $field['resolve']; + } + + $method = studly_case(sprintf('resolve-%s-%field', $name)); + + if (method_exists($type, $method)) { + return function() use ($type, $method) { return [$type, $method]; }; + } + + return null; + } +} diff --git a/src/Transformer/Type/ModelTransformer.php b/src/Transformer/Type/ModelTransformer.php new file mode 100644 index 0000000..f989944 --- /dev/null +++ b/src/Transformer/Type/ModelTransformer.php @@ -0,0 +1,74 @@ + $this->getName($instance), + 'description' => $this->getDescription($instance), + 'fields' => $this->getFields($instance) + ]); + } + + /** + * Return name of given model + * + * @param Model $model + * @return string + */ + private function getName(Model $model) { + if ($model instanceof ModelAttributes) { + return $model->getObjectName(); + } + + return ucfirst(with(new \ReflectionClass($model))->getShortName()); + } + + /** + * Return model description + * + * @param Model $model + * @return string + */ + private function getDescription(Model $model) { + if ($model instanceof ModelAttributes) { + return $model->getObjectDescription(); + } + + return sprintf('A %s model representation', $this->getName($model)); + } + + /** + * TODO + * + * Return corresponding fields. We're prefer using callable here because of + * recursive models. As this method handles relationships, we have to manage + * all depths cases + * + * @param Model $model + * @return callable + */ + private function getFields(Model $model) { + return []; + } +} From 63a4eaf12a45400a9934ae3f4fbd5a02b6a773cc Mon Sep 17 00:00:00 2001 From: Cyril Mizzi Date: Tue, 1 Aug 2017 14:50:16 +0200 Subject: [PATCH 03/11] overrides default configuration --- resources/config.php | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/resources/config.php b/resources/config.php index fddf9a2..0f09c74 100644 --- a/resources/config.php +++ b/resources/config.php @@ -17,21 +17,40 @@ ] ], - // Type configuration. It allows you to define custom Type based on - // StudioNet\GraphQL\Type\{EloquentType,Type} - // - // `entities` : list of entities - // `definitions` : list of custom Type - 'type' => [ - 'entities' => [], - 'definitions' => [] - ], + // Type configuration. You can append any data : a transformer will handle + // them (if exists) + 'type' => [], // Scalar field definitions 'scalar' => [ \StudioNet\GraphQL\Support\Scalar\Timestamp::class ], + // A transformer handles a supports and a transform method. I can convert + // any type of data in specific content. In order to make work Eloquent + // models, a transformer convert it into specific ObjectType. + // + // Take care about order : the first supported transformer will handle the + // transformation ; others will simply not be called. I you want make + // modifications about a specific transformer, you'll have to extend + // existing one and replace it below + // + // There's 3 types of transformers : type, query and mutation + 'transformer' => [ + 'type' => [ + \StudioNet\GraphQL\Transformer\Type\ModelTransformer::class, + \StudioNet\GraphQL\Transformer\Type\DefaultTransformer::class + ], + 'query' => [ + \StudioNet\GraphQL\Transformer\Query\ModelTransformer::class, + \StudioNet\GraphQL\Transformer\Query\DefaultTransformer::class + ], + 'mutation' => [ + \StudioNet\GraphQL\Transformer\Mutation\ModelTransformer::class, + \StudioNet\GraphQL\Transformer\Mutation\DefaultTransformer::class + ] + ], + // Response configuration // // `headers` : custom headers to send on controller response From 4199faaf01db8ee4ccf7083e2d5c832657705b40 Mon Sep 17 00:00:00 2001 From: Cyril Mizzi Date: Tue, 1 Aug 2017 14:50:27 +0200 Subject: [PATCH 04/11] starts using transformers --- src/GraphQL.php | 127 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 102 insertions(+), 25 deletions(-) diff --git a/src/GraphQL.php b/src/GraphQL.php index 78128ce..c833751 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -28,6 +28,13 @@ class GraphQL { /** @var ScalarType[] $scalars */ private $scalars = []; + /** @var array $transformers */ + private $transformers = [ + 'type' => [], + 'query' => [], + 'mutation' => [] + ]; + /** * __construct * @@ -226,7 +233,6 @@ public function registerSchema($name, array $data) { $this->schemas[$name] = array_merge([ 'query' => [], 'mutation' => [], - 'entities' => [], ], $data); } @@ -238,36 +244,55 @@ public function registerSchema($name, array $data) { * * @return void */ - public function registerType($name, $type) { - if (is_string($type)) { - $type = $this->app->make($type); - } + public function registerType($type) { + $types = $this->applyTransformers('type', $type); + + // As you're able to transform multiple time the same object (have + // multiple objects from same one), we have to while until the last one + foreach ($types as $object) { + // Assert that the given type extend from TypeInterface or is an + // instance of ObjectType + if ((!$object instanceof ObjectType)) { + throw new Exception\TypeException('Given type doesn\'t extends from ObjectType'); + } - // Assert that the given type extend from TypeInterface or is an - // instance of ObjectType - if ((!$type instanceof ObjectType) and (!$type instanceof TypeInterface)) { - throw new Exception\TypeException('Given type doesn\'t extend from TypeInterface'); - } + // Try to register the object as query and mutation. For example, + // it's useful in order to generate query and mutation + // for EloquentObjectType + try { + $this->registerQuery($object); + $this->registerMutation($object); + } catch (\Exception $e) {} + + // If there's no name, just guess it from built ObjectType or fallback + // on type name + if (empty($type->name)) { + $name = with(new \ReflectionClass($type))->getShortName(); + $type->name = $name; + } - // Assert name is not empty : otherwise, get the class name from type - if (empty($name) or is_numeric($name)) { - $name = with(new \ReflectionClass($type))->getShortName(); + $this->types[strtolower($type->name)] = $type; } + } - // If the type is extended from TypeInterface, we know that he has a - // `toType` method : so let's call it in order to retrieve an ObjectType - if ($type instanceof TypeInterface) { - $type = $type->toType(); - } + /** + * Register a global query + * + * @param string|ObjectType $query + * @return void + */ + public function registerQuery($query) { + $queries = $this->applyTransformers('query', $query); - // As we're working with generated types, we can't allow override - // because user will be lost. So let's throw an exception when this - // case is here - if (array_key_exists($name, $this->types)) { - throw new Exception\TypeException('Cannot override existing type'); - } + foreach ($queries as $object) { + if (!is_array($object)) { + throw new Exception\QueryException('A query must be an array'); + } - $this->types[strtolower($name)] = $type; + foreach ($this->schemas as $key => $schema) { + $this->schemas[$key]['query'] = array_merge($object, $schema['query']); + } + } } /** @@ -296,6 +321,58 @@ public function registerScalar($name, $scalar) { $this->scalars[strtolower($name)] = $scalar; } + /** + * Register transformer. A transformer performs transactions between an + * Object to another. Each transformer is applied on specific type of data : + * type, query or mutation. It cannot handle either. + * + * @param string $category + * @param string $transformer + * @return void + */ + public function registerTransformer($category, $transformer) { + if (!in_array($category, ['type', 'query', 'mutation'])) { + throw new Exception\TransformerException('Unable to find given category'); + } + + $this->transformers[$category][] = $this->app->make($transformer); + } + + /** + * Apply transformation. When a transformer can handle the given class, the + * while will break and return the current state + * + * @param string $type + * @param mixed $cls + * @return mixed + */ + private function applyTransformers($type, $cls) { + if (!array_key_exists($type, $this->transformers)) { + throw new Exception\TransformerException('Cannot transform given type'); + } + + // Convert string to instance if possible + if (is_string($cls)) { + $cls = $this->app->make($cls); + } + + $data = []; + + foreach ($this->transformers[$type] as $transformer) { + if ($transformer->supports($cls)) { + $data[] = $transformer->transform($cls); + } + } + + // No transformer was found. Let's throw an error : the given class is + // not supported at all + if (empty($data)) { + throw new Exception\TransformerNotFoundException('There\'s no transformer for given class'); + } + + return $data; + } + /** * Assert schema exists * From acae5a758e7c8ca0e902e19ed551f6455f961e0a Mon Sep 17 00:00:00 2001 From: Cyril Mizzi Date: Wed, 2 Aug 2017 11:57:45 +0200 Subject: [PATCH 05/11] adds availabled transformers --- resources/config.php | 8 +- src/GraphQL.php | 91 ++---- src/ServiceProvider.php | 20 +- src/Support/Field.php | 16 +- src/Support/FieldInterface.php | 14 + src/Transformer/FieldTransformer.php | 36 +++ src/Transformer/Transformer.php | 2 +- src/Transformer/Type/ModelTransformer.php | 270 +++++++++++++++++- ...ultTransformer.php => TypeTransformer.php} | 36 ++- 9 files changed, 377 insertions(+), 116 deletions(-) create mode 100644 src/Transformer/FieldTransformer.php rename src/Transformer/{Type/DefaultTransformer.php => TypeTransformer.php} (65%) diff --git a/resources/config.php b/resources/config.php index 0f09c74..8b4f982 100644 --- a/resources/config.php +++ b/resources/config.php @@ -39,15 +39,13 @@ 'transformer' => [ 'type' => [ \StudioNet\GraphQL\Transformer\Type\ModelTransformer::class, - \StudioNet\GraphQL\Transformer\Type\DefaultTransformer::class + \StudioNet\GraphQL\Transformer\TypeTransformer::class, ], 'query' => [ - \StudioNet\GraphQL\Transformer\Query\ModelTransformer::class, - \StudioNet\GraphQL\Transformer\Query\DefaultTransformer::class + \StudioNet\GraphQL\Transformer\FieldTransformer::class, ], 'mutation' => [ - \StudioNet\GraphQL\Transformer\Mutation\ModelTransformer::class, - \StudioNet\GraphQL\Transformer\Mutation\DefaultTransformer::class + \StudioNet\GraphQL\Transformer\FieldTransformer::class, ] ], diff --git a/src/GraphQL.php b/src/GraphQL.php index c833751..e8cf6f0 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -56,17 +56,6 @@ public function getSchema($name) { throw new Exception\SchemaNotFoundException('Cannot find schema ' . $name); } - // This method is called only when `execute()` method is called. So, we - // can initialize all our entities right here without problems - // - // TODO I don't really like to see this here... Must be refactored later - $manager = $this->app->make('graphql.eloquent.type_manager'); - $models = config('graphql.type.entities', []); - - foreach ($manager->fromModels($models) as $key => $type) { - $this->registerType($key, $type); - } - // Represents an array like // // [ @@ -156,9 +145,7 @@ public function execute($query, $variables = [], $opts = []) { * @return array */ public function manageQuery(array $queries) { - $data = []; - $models = config('graphql.type.entities', []); - $manager = $this->app->make('graphql.eloquent.query_manager'); + $data = []; // Parse each query class and build it within the ObjectType foreach ($queries as $name => $query) { @@ -170,14 +157,6 @@ public function manageQuery(array $queries) { $data = $data + [$name => $query->toArray()]; } - // Parse each model, retrieve is corresponding generated type and build - // a generic query upon it - foreach ($models as $model) { - $table = str_singular($this->app->make($model)->getTable()); - $type = $this->type($table); - $data = $data + $manager->fromType($table, $type); - } - return new ObjectType([ 'name' => 'Query', 'fields' => $data @@ -244,55 +223,23 @@ public function registerSchema($name, array $data) { * * @return void */ - public function registerType($type) { - $types = $this->applyTransformers('type', $type); - - // As you're able to transform multiple time the same object (have - // multiple objects from same one), we have to while until the last one - foreach ($types as $object) { - // Assert that the given type extend from TypeInterface or is an - // instance of ObjectType - if ((!$object instanceof ObjectType)) { - throw new Exception\TypeException('Given type doesn\'t extends from ObjectType'); - } - - // Try to register the object as query and mutation. For example, - // it's useful in order to generate query and mutation - // for EloquentObjectType - try { - $this->registerQuery($object); - $this->registerMutation($object); - } catch (\Exception $e) {} - - // If there's no name, just guess it from built ObjectType or fallback - // on type name - if (empty($type->name)) { - $name = with(new \ReflectionClass($type))->getShortName(); - $type->name = $name; - } + public function registerType($name, $type) { + $type = $this->applyTransformers('type', $type); - $this->types[strtolower($type->name)] = $type; + // Assert that the given type extend from TypeInterface or is an + // instance of typeType + if ((!$type instanceof ObjectType)) { + throw new Exception\TypeException('Given type doesn\'t extends from typeType'); } - } - - /** - * Register a global query - * - * @param string|ObjectType $query - * @return void - */ - public function registerQuery($query) { - $queries = $this->applyTransformers('query', $query); - - foreach ($queries as $object) { - if (!is_array($object)) { - throw new Exception\QueryException('A query must be an array'); - } - foreach ($this->schemas as $key => $schema) { - $this->schemas[$key]['query'] = array_merge($object, $schema['query']); - } + // If there's no name, just guess it from built typeType or fallback + // on type name + if (empty($name) and empty($type->name)) { + $name = with(new \ReflectionClass($type))->getShortName(); + $type->name = $name; } + + $this->types[strtolower($type->name)] = $type; } /** @@ -356,21 +303,15 @@ private function applyTransformers($type, $cls) { $cls = $this->app->make($cls); } - $data = []; - foreach ($this->transformers[$type] as $transformer) { if ($transformer->supports($cls)) { - $data[] = $transformer->transform($cls); + return $transformer->transform($cls); } } // No transformer was found. Let's throw an error : the given class is // not supported at all - if (empty($data)) { - throw new Exception\TransformerNotFoundException('There\'s no transformer for given class'); - } - - return $data; + throw new Exception\TransformerNotFoundException('There\'s no transformer for given class'); } /** diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 0b838dc..f4e3ba6 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -23,9 +23,10 @@ public function boot() { $this->publishes([$config => config_path('graphql.php')]); // Call external methods to load defined schemas and others things + $this->registerScalars(); + $this->registerTransformers(); $this->registerSchemas(); $this->registerTypes(); - $this->registerScalars(); } /** @@ -47,7 +48,7 @@ public function registerSchemas() { * @return void */ public function registerTypes() { - $types = config('graphql.type.definitions', []); + $types = config('graphql.type', []); foreach ($types as $name => $type) { $this->app['graphql']->registerType($name, $type); @@ -67,6 +68,21 @@ public function registerScalars() { } } + /** + * Register transformers + * + * @return void + */ + public function registerTransformers() { + $transformers = config('graphql.transformer', []); + + foreach ($transformers as $key => $many) { + foreach ($many as $transformer) { + $this->app['graphql']->registerTransformer($key, $transformer); + } + } + } + /** * Register the application services. * diff --git a/src/Support/Field.php b/src/Support/Field.php index 2a6aac4..554e291 100644 --- a/src/Support/Field.php +++ b/src/Support/Field.php @@ -1,20 +1,22 @@ getAttributes() + [ + 'type' => $instance->getRelatedType(), + 'args' => $instance->getArguments() + ]; + + if (method_exists($instance, 'getResolver')) { + $attributes = $attributes + [ + 'resolve' => [$this, 'resolve'] + ]; + } + + return $attributes; + } +} diff --git a/src/Transformer/Transformer.php b/src/Transformer/Transformer.php index ae4fb1b..1931fbf 100644 --- a/src/Transformer/Transformer.php +++ b/src/Transformer/Transformer.php @@ -11,7 +11,7 @@ */ abstract class Transformer implements TransformerInterface { /** @var Application $app */ - private $app; + protected $app; /** * __construct diff --git a/src/Transformer/Type/ModelTransformer.php b/src/Transformer/Type/ModelTransformer.php index f989944..b1ad56c 100644 --- a/src/Transformer/Type/ModelTransformer.php +++ b/src/Transformer/Type/ModelTransformer.php @@ -1,17 +1,42 @@ connection = $connection; + parent::__construct($application); + } + /** * {@inheritDoc} */ @@ -23,11 +48,18 @@ public function supports($instance) { * {@inheritDoc} */ public function transform($instance) { - return new EloquentObjectType([ - 'name' => $this->getName($instance), - 'description' => $this->getDescription($instance), - 'fields' => $this->getFields($instance) - ]); + $key = 'type:' . $instance->getTable(); + + if (empty($this->cache[$key])) { + $this->cache[$key] = new EloquentObjectType([ + 'name' => $this->getName($instance), + 'description' => $this->getDescription($instance), + 'fields' => $this->getFields($instance), + 'model' => $instance + ]); + } + + return $this->cache[$key]; } /** @@ -67,8 +99,232 @@ private function getDescription(Model $model) { * * @param Model $model * @return callable + * @see github.com/webonyx/graphql-php/blob/master/docs/type-system/object-types.md#field-configuration-options */ private function getFields(Model $model) { - return []; + $columns = $this->getColumns($model); + $relations = $this->getRelations($model); + + return function() use ($model, $columns, $relations) { + $fields = []; + + foreach ($columns as $column => $type) { + $field = [ + 'name' => $column, + 'description' => title_case(preg_replace('/_/', ' ', $column)), + ]; + + // We have a relationship field here ! + if (array_key_exists($column, $relations)) { + // Get relationship + $relation = $relations[$column]; + $related = $this->app->make($relation['model']); + + // Get related type (if doesn't exists, it will be + // generated) + $many = false; + $type = $this->transform($related); + + // Build relationship : how to know if we have to return + // a listOf or directly the type ? With the known + // relationship ! If we have a `HasMany` relationship, + // we're able to know that we have to return many type + // at once + switch ($relation['type']) { + case 'HasMany' : $many = true; break; + } + + // Only append arguments if not empty. Some relations + // like `BelongsTo` doesn't handle arguments (we can't + // lookup throw a single entry, even with id) + if ($many) { + $type = GraphQLType::listOf($type); + $field = array_merge([ + 'args' => $this->getArguments(), + 'resolve' => $this->getResolver($relation) + ], $field); + } + + unset($relations[$column]); + } + + // If the value still null, we can't use it : just continue + // without doing anything + if (is_null($type)) { + continue; + } + + // Apply modifications into global fields array + $fields[] = ['type' => $type] + $field; + } + + return $fields; + }; + } + + /** + * Resolve a relationship field + * + * @param array $relation + * @return callable + */ + private function getResolver(array $relation) { + $method = $relation['field']; + + return function($root, array $args) use ($method) { + $collection = $root->{$method}; + + foreach ($args as $key => $value) { + switch ($key) { + case 'after' : $collection = $collection->where($primary, '>', $value) ; break; + case 'before' : $collection = $collection->where($primary, '<', $value) ; break; + case 'skip' : $collection = $collection->skip($value) ; break; + case 'take' : $collection = $collection->take($value) ; break; + } + } + + return $collection->all(); + }; + } + + /** + * Return available arguments (many because there's no argument for single + * element) + * + * @return array + */ + private function getArguments() { + return [ + 'after' => ['type' => GraphQLType::id() , 'description' => 'Based-cursor navigation' ] , + 'before' => ['type' => GraphQLType::id() , 'description' => 'Based-cursor navigation' ] , + 'skip' => ['type' => GraphQLType::int() , 'description' => 'Offset-based navigation' ] , + 'take' => ['type' => GraphQLType::int() , 'description' => 'Limit-based navigation' ] , + ]; + } + + /** + * Return available columns for given Model ; it also append relationships + * fields : it's virtual within the database but real in GraphQL schema + * + * @param Model $model + * @return array + */ + private function getColumns(Model $model) { + // Handle cache management + $table = $model->getTable(); + $key = 'columns:' . $table; + + if (empty($this->cache[$key])) { + $data = []; + $primary = $model->getKeyName(); + $columns = $this->connection->getSchemaBuilder()->getColumnListing($table); + + // Remove hidden columns : we don't want show or update them. Also + // append relationships virtual columns + $related = $this->getRelations($model); + $columns = array_diff($columns, $model->getHidden()); + $columns = array_merge(array_keys($related), $columns); + + foreach (array_unique($columns) as $column) { + try { + $type = $this->connection->getDoctrineColumn($table, $column); + $type = $type->getType(); + } catch (SchemaException $e) { + // There's nothing left to do (it's a virtual field or, it + // also could append with PostgreSQL multiple schemas) + $data[$column] = null; + continue; + } + + // Parse each available database data type and call is related + // GraphQL type + switch ($type->getName()) { + case 'smallint' : + case 'bigint' : + case 'integer' : $type = GraphQLType::int() ; break; + case 'decimal' : + case 'float' : $type = GraphQLType::float() ; break; + case 'date' : + case 'datetimetz' : + case 'time' : + case 'datetime' : $type = $this->app['graphql']->scalar('timestamp') ; break; + case 'array' : + case 'simple_array' : $type = GraphQLType::listOf(GraphQLType::string()) ; break; + default : $type = GraphQLType::string() ; break; + } + + // Assert primary key is an id + if ($column === $primary) { + $type = GraphQLType::id(); + } + + $data[$column] = $type; + } + + $this->cache[$key] = $data; + } + + return $this->cache[$key]; + } + + /** + * Return model relationships + * + * @param Model $model + * @return array + */ + private function getRelations(Model $model) { + // Handle cache managment + $key = 'relation:' . get_class($model); + + if (empty($this->cache[$key])) { + $relations = []; + $reflection = new \ReflectionClass($model); + $traits = $reflection->getTraits(); + $exclude = []; + + // Get traits methods and append them to the excluded methods + foreach ($traits as $trait) { + foreach ($trait->getMethods() as $method) { + $exclude[$method->getName()] = true; + } + } + + foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + if ($method->class !== get_class($model)) { + continue; + } + + // We don't want method with parameters (relationship doesn't have + // parameter) + if (!empty($method->getParameters())) { + continue; + } + + // We don't want parsing this current method + if (array_key_exists($method->getName(), $exclude)) { + continue; + } + + try { + $return = $method->invoke($model); + + // Get only method that returned Relation instance + if ($return instanceof Relation) { + $name = $method->getName(); + + $relations[$name] = [ + 'field' => $method->getName(), + 'type' => (new \ReflectionClass($return))->getShortName(), + 'model' => (new \ReflectionClass($return->getRelated()))->getName() + ]; + } + } catch (\ErrorException $e) {} + } + + $this->cache[$key] = $relations; + } + + return $this->cache[$key]; } } diff --git a/src/Transformer/Type/DefaultTransformer.php b/src/Transformer/TypeTransformer.php similarity index 65% rename from src/Transformer/Type/DefaultTransformer.php rename to src/Transformer/TypeTransformer.php index 342ce43..948a046 100644 --- a/src/Transformer/Type/DefaultTransformer.php +++ b/src/Transformer/TypeTransformer.php @@ -1,37 +1,34 @@ getFields(); - $attributes = $type->getAttributes(); - $interfaces = $type->getInterfaces(); + public function transform($instance) { + $fields = $instance->getFields(); + $attributes = $instance->getAttributes(); + $interfaces = $instance->getInterfaces(); foreach ($fields as $key => $field) { if (is_array($field)) { - $resolver = $this->getFieldResolver($type, $key, $field); + $resolver = $this->getFieldResolver($instance, $key, $field); if ($resolver !== null) { $fields[$key]['resolve'] = $resolver; @@ -39,13 +36,14 @@ public function transform(TypeInterface $type) { } } + // Merge all attributes within attributes var $attributes = array_merge($attributes, [ - 'fields' => $fields, - 'name' => $this->getName(), + 'fields' => $fields, + 'name' => $this->getName(), 'description' => $this->getDescription() ]); - if (!empty($nterfaces)) { + if (!empty($interfaces)) { $attributes['interfaces'] = $interfaces; } From da25a44d6b925510e1928c5490f8b633fd3c8965 Mon Sep 17 00:00:00 2001 From: Cyril Mizzi Date: Wed, 2 Aug 2017 12:01:56 +0200 Subject: [PATCH 06/11] creates transformer exception --- TransformerException.php | 9 +++++++++ TransformerNotFoundException.php | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 TransformerException.php create mode 100644 TransformerNotFoundException.php diff --git a/TransformerException.php b/TransformerException.php new file mode 100644 index 0000000..9dd85b3 --- /dev/null +++ b/TransformerException.php @@ -0,0 +1,9 @@ + Date: Wed, 2 Aug 2017 12:02:02 +0200 Subject: [PATCH 07/11] removes managers --- src/Eloquent/Manager.php | 167 ------------------------------- src/Eloquent/MutationManager.php | 90 ----------------- src/Eloquent/QueryManager.php | 99 ------------------ src/Eloquent/TypeManager.php | 131 ------------------------ src/ServiceProvider.php | 18 +--- 5 files changed, 1 insertion(+), 504 deletions(-) delete mode 100644 src/Eloquent/Manager.php delete mode 100644 src/Eloquent/MutationManager.php delete mode 100644 src/Eloquent/QueryManager.php delete mode 100644 src/Eloquent/TypeManager.php diff --git a/src/Eloquent/Manager.php b/src/Eloquent/Manager.php deleted file mode 100644 index 0dcab02..0000000 --- a/src/Eloquent/Manager.php +++ /dev/null @@ -1,167 +0,0 @@ -app = $app; - } - - /** - * Return columns name for given model - * - * @param Model $model - * @param array $include - * @return array - */ - protected function getColumns(Model $model, array $include = []) { - $key = 'columns:' . get_class($model); - - if (empty($this->cache[$key])) { - $table = $model->getTable(); - $primary = $model->getKeyName(); - $doctrine = \DB::connection(); - $columns = \Schema::getColumnListing($model->getTable()); - $columns = array_diff($columns, $model->getHidden()); - $columns = array_merge($columns, array_keys($include)); - $data = []; - - foreach (array_unique($columns) as $column) { - try { - $type = $doctrine->getDoctrineColumn($table, $column)->getType(); - } catch (SchemaException $e) { - $data[$column] = null; - continue; - } - - switch ($type->getName()) { - case 'smallint' : - case 'bigint' : - case 'integer' : $type = GraphQLType::int() ; break; - case 'decimal' : - case 'float' : $type = GraphQLType::float() ; break; - case 'date' : - case 'datetimetz' : - case 'time' : - case 'datetime' : $type = \GraphQL::scalar('timestamp') ; break; - case 'array' : - case 'simple_array' : $type = GraphQLType::listOf(GraphQLType::string()) ; break; - default : $type = GraphQLType::string() ; break; - } - - // Assert primary key is an id - if ($column === $primary) { - $type = GraphQLType::id(); - } - - $data[$column] = $type; - } - - $this->cache[$key] = $data; - } - - return $this->cache[$key]; - } - - /** - * Return availabled arguments - * - * @param bool $plural - * @return array - */ - protected function getArguments($plural = false) { - if ($plural === false) { - return [ - 'id' => ['type' => GraphQLType::nonNull(GraphQLType::id()), 'description' => 'Primary key lookup'] - ]; - } - - return [ - 'after' => ['type' => GraphQLType::id() , 'description' => 'Based-cursor navigation' ] , - 'before' => ['type' => GraphQLType::id() , 'description' => 'Based-cursor navigation' ] , - 'skip' => ['type' => GraphQLType::int() , 'description' => 'Offset-based navigation' ] , - 'take' => ['type' => GraphQLType::int() , 'description' => 'Limit-based navigation' ] , - ]; - } - - - /** - * Return relationships - * - * @param Model $model - * @return array - */ - protected function getRelations(Model $model) { - $key = 'relation:' . get_class($model); - - if (empty($this->cache[$key])) { - $relations = []; - $reflection = new \ReflectionClass($model); - $traits = $reflection->getTraits(); - $exclude = []; - - // Get traits methods and append them to the excluded methods - foreach ($traits as $trait) { - foreach ($trait->getMethods() as $method) { - $exclude[$method->getName()] = true; - } - } - - foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { - if ($method->class !== get_class($model)) { - continue; - } - - // We don't want method with parameters (relationship doesn't have - // parameter) - if (!empty($method->getParameters())) { - continue; - } - - // We don't want parsing this current method - if (array_key_exists($method->getName(), $exclude)) { - continue; - } - - try { - $return = $method->invoke($model); - - // Get only method that returned Relation instance - if ($return instanceof Relation) { - $name = $method->getName(); - - $relations[$name] = [ - 'field' => $method->getName(), - 'type' => (new ReflectionClass($return))->getShortName(), - 'model' => (new ReflectionClass($return->getRelated()))->getName() - ]; - } - } catch (ErrorException $e) {} - } - - $this->cache[$key] = $relations; - } - - return $this->cache[$key]; - } -} diff --git a/src/Eloquent/MutationManager.php b/src/Eloquent/MutationManager.php deleted file mode 100644 index f9562dc..0000000 --- a/src/Eloquent/MutationManager.php +++ /dev/null @@ -1,90 +0,0 @@ -config['model']; - - return [ - 'resolve' => $this->getResolver($model), - 'args' => $this->getColumns($model), - 'type' => $type - ]; - } - - /** - * Return arguments - * - * @param Model $model - * @param array $include - * @return array - */ - protected function getColumns(Model $model, array $include = []) { - $key = 'mutation:columns:' . get_class($model); - - if (empty($this->cache[$key])) { - $data = []; - $columns = parent::getColumns($model, $include); - $fillable = array_flip($model->getFillable()); - $hidden = array_flip($model->getHidden()); - $primary = $model->getKeyName(); - - if (!empty($fillable)) { - $columns = array_intersect_key($columns, $fillable); - } - - if (!empty($hidden)) { - $columns = array_diff_key($columns, $hidden); - } - - if (!array_key_exists($primary, $columns)) { - $data[$primary] = GraphQLType::id(); - } - - // Parse each column in order to know which is fillable. To allow - // model to be updated, we have to use a uniq id : the id - foreach ($columns as $column => $type) { - $data[$column] = ['type' => $type]; - } - - $this->cache[$key] = $data; - } - - return $this->cache[$key]; - } - - /** - * Resolve mutation - * - * @param Model $model - * @return Model - * - * @SuppressWarnings(PHPMD.UnusedLocalVariable) - */ - protected function getResolver(Model $model) { - return function($root, array $args) use ($model) { - $primary = $model->getKeyName(); - $data = $model->query()->updateOrCreate( - [$primary => $args[$primary]], - $args - ); - - return $data; - }; - } -} diff --git a/src/Eloquent/QueryManager.php b/src/Eloquent/QueryManager.php deleted file mode 100644 index 56b54ab..0000000 --- a/src/Eloquent/QueryManager.php +++ /dev/null @@ -1,99 +0,0 @@ - $this->toSingle($type), - $plural => $this->toMany($type) - ]; - } - - /** - * Return a query that will return a single ObjectType - * - * @param ObjectType $type - * @return array - */ - private function toSingle(ObjectType $type) { - $model = $type->config['model']; - - return [ - 'resolve' => $this->getResolver($model), - 'args' => $this->getArguments(), - 'type' => $type - ]; - } - - /** - * Return a query that will return a ListOf - * - * @param ObjectType $type - * @return array - */ - private function toMany(ObjectType $type) { - $model = $type->config['model']; - - return [ - 'resolve' => $this->getResolver($model), - 'args' => $this->getArguments(true), - 'type' => GraphQLType::listOf($type) - ]; - } - - /** - * Return resolver for given model - * - * @param Model $model - * @return callable - * - * @SuppressWarnings(PHPMD.UnusedLocalVariable) - */ - private function getResolver(Model $model) { - $relations = $this->getRelations($model); - - return function($root, array $args, $context, ResolveInfo $info) use ($model, $relations) { - $primary = $model->getKeyName(); - $builder = $model->newQuery(); - $fields = $info->getFieldSelection(3); - $common = array_intersect_key($fields, $relations); - - if (!empty($common)) { - foreach (array_keys($common) as $related) { - $builder->with($related); - } - } - - // Retrieve single node - if (array_key_exists($primary, $args)) { - return $builder->findOrFail($args[$primary]); - } - - foreach ($args as $key => $value) { - switch ($key) { - case 'after' : $builder->where($primary, '>', $value) ; break; - case 'before' : $builder->where($primary, '<', $value) ; break; - case 'skip' : $builder->skip($value) ; break; - case 'take' : $builder->take($value) ; break; - } - } - - return $builder->get(); - }; - } -} diff --git a/src/Eloquent/TypeManager.php b/src/Eloquent/TypeManager.php deleted file mode 100644 index 1ac1bb8..0000000 --- a/src/Eloquent/TypeManager.php +++ /dev/null @@ -1,131 +0,0 @@ - $model) { - $model = $this->app->make($model); - - // If user doesn't specified a key to access entity, simply use - // entity's table name - if (is_numeric($key)) { - $key = str_singular($model->getTable()); - } - - $types[$key] = $this->toType($model); - } - - return $types; - } - - /** - * Convert a Model to an ObjectType - * - * @param Model $model - * @return ObjectType - */ - public function toType(Model $model) { - $table = str_singular($model->getTable()); - $key = 'type:type:' . $table; - - if (empty($this->cache[$key])) { - $relations = $this->getRelations($model); - $columns = $this->getColumns($model, $relations); - $fields = function() use ($columns, $relations) { - $fields = []; - - foreach ($columns as $column => $type) { - $field = []; - - // We have a relationship field here ! - if (array_key_exists($column, $relations)) { - $relation = $relations[$column]; - $related = $this->app->make($relation['model']); - $table = $related->getTable(); - $pluralize = false; - $type = $this->toType($related); - - // Build relationship : how to know if we have to return - // a listOf or directly the type ? With the known - // relationship ! If we have a `HasMany` relationship, - // we're able to know that we have to return many type - // at once - switch ($relation['type']) { - case 'HasMany' : $pluralize = true; break; - } - - // Only append arguments if not empty. Some relations - // like `BelongsTo` doesn't handle arguments (we can't - // lookup throw a single entry, even with id) - if ($pluralize) { - $type = GraphQLType::listOf($type); - $field = array_merge([ - 'args' => $this->getArguments(true), - 'resolve' => $this->getResolver($relation) - ], $field); - } - - unset($relations[$column]); - } - - if (is_null($type)) { - continue; - } - - $fields[$column] = array_merge($field, [ - 'description' => title_case(preg_replace('/_/', ' ', $column)), - 'type' => $type - ]); - } - - return $fields; - }; - - $this->cache[$key] = new ObjectType([ - 'name' => $table, - 'model' => $model, - 'fields' => $fields - ]); - } - - return $this->cache[$key]; - } - - /** - * Return a type resolver from given relation - * - * @param array $relation - * @return callable - */ - private function getResolver(array $relation) { - $method = $relation['field']; - - return function($root, array $args) use ($method) { - $collection = $root->{$method}; - - foreach ($args as $key => $value) { - switch ($key) { - case 'after' : $collection = $collection->where($primary, '>', $value) ; break; - case 'before' : $collection = $collection->where($primary, '<', $value) ; break; - case 'skip' : $collection = $collection->skip($value) ; break; - case 'take' : $collection = $collection->take($value) ; break; - } - } - - return $collection->all(); - }; - } -} diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index f4e3ba6..3b52b96 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -1,9 +1,6 @@ app->singleton(MutationManager::class, function($app) { return new MutationManager($app); }); - $this->app->singleton(QueryManager::class, function($app) { return new QueryManager($app); }); - $this->app->singleton(TypeManager::class, function($app) { return new TypeManager($app); }); $this->app->singleton(GraphQL::class, function($app) { return new GraphQL($app); }); - $this->app->bind('graphql', GraphQL::class); - $this->app->bind('graphql.eloquent.type_manager', TypeManager::class); - $this->app->bind('graphql.eloquent.query_manager', QueryManager::class); - $this->app->bind('graphql.eloquent.mutation_manager', MutationManager::class); } /** @@ -107,12 +96,7 @@ public function register() { * @return array */ public function provides() { - return [ - 'graphql' , GraphQL::class , - 'graphql.query_manager' , QueryManager::class , - 'graphql.mutation_manager' , MutationManager::class , - 'graphql.eloquent.type_manager' , TypeManager::class , - ]; + return ['graphql', GraphQL::class]; } /** From a169fa02411b08572a4ebc7375f76ddc7d674017 Mon Sep 17 00:00:00 2001 From: Cyril Mizzi Date: Wed, 2 Aug 2017 12:02:28 +0200 Subject: [PATCH 08/11] replaces manager call with transformers --- src/GraphQL.php | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/GraphQL.php b/src/GraphQL.php index e8cf6f0..1087434 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -153,8 +153,8 @@ public function manageQuery(array $queries) { $name = strtolower(with(new \ReflectionClass($query))->getShortName()); } - $query = $this->app->make($query); - $data = $data + [$name => $query->toArray()]; + $query = $this->applyTransformers('query', $query); + $data = $data + [$name => $query]; } return new ObjectType([ @@ -172,9 +172,7 @@ public function manageQuery(array $queries) { * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function manageMutation(array $mutations) { - $data = []; - $models = config('graphql.type.entities', []); - $manager = $this->app->make('graphql.eloquent.mutation_manager'); + $data = []; // Parse each query class and build it within the ObjectType foreach ($mutations as $name => $mutation) { @@ -182,16 +180,8 @@ public function manageMutation(array $mutations) { $name = strtolower(with(new \ReflectionClass($mutation))->getShortName()); } - $mutation = $this->app->make($mutation); - $data = $data + [$name => $mutation->toArray()]; - } - - // Parse each model, retrieve is corresponding generated type and build - // a generic mutation upon it - foreach ($models as $model) { - $table = str_singular($this->app->make($model)->getTable()); - $type = $this->type($table); - $data[$table] = $manager->fromType($type); + $mutation = $this->applyTransformers('mutation', $mutation); + $data = $data + [$name => $mutation]; } return new ObjectType([ From 6f40f4877acb9045266c2d1765aa14be6881f9d8 Mon Sep 17 00:00:00 2001 From: Cyril Mizzi Date: Wed, 2 Aug 2017 12:21:34 +0200 Subject: [PATCH 09/11] fixes unit tests --- src/GraphQL.php | 24 +++++++++++++++++++----- tests/GraphQLTest.php | 5 +++-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/GraphQL.php b/src/GraphQL.php index 1087434..6a5d0cc 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -153,8 +153,13 @@ public function manageQuery(array $queries) { $name = strtolower(with(new \ReflectionClass($query))->getShortName()); } - $query = $this->applyTransformers('query', $query); - $data = $data + [$name => $query]; + try { + $query = $this->applyTransformers('query', $query); + } catch (\ReflectionException $e) { + throw new Exception\TypeNotFoundException($e->getMessage()); + } + + $data = $data + [$name => $query]; } return new ObjectType([ @@ -180,7 +185,12 @@ public function manageMutation(array $mutations) { $name = strtolower(with(new \ReflectionClass($mutation))->getShortName()); } - $mutation = $this->applyTransformers('mutation', $mutation); + try { + $mutation = $this->applyTransformers('mutation', $mutation); + } catch (\ReflectionException $e) { + throw new Exception\TypeNotFoundException($e->getMessage()); + } + $data = $data + [$name => $mutation]; } @@ -209,12 +219,16 @@ public function registerSchema($name, array $data) { * Register a type * * @param string|null $name - * @param string|ObjectType|TypeInterface $type + * @param mixed $type * * @return void */ public function registerType($name, $type) { - $type = $this->applyTransformers('type', $type); + try { + $type = $this->applyTransformers('type', $type); + } catch (\ReflectionException $e) { + throw new Exception\TypeNotFoundException($e->getMessage()); + } // Assert that the given type extend from TypeInterface or is an // instance of typeType diff --git a/tests/GraphQLTest.php b/tests/GraphQLTest.php index 0d02ec7..8d021fc 100644 --- a/tests/GraphQLTest.php +++ b/tests/GraphQLTest.php @@ -2,8 +2,9 @@ namespace StudioNet\GraphQL\Tests; use GraphQL\Type\Definition\Type as GraphQLType; -use StudioNet\GraphQL\GraphQL; use Illuminate\Foundation\Testing\DatabaseTransactions; +use StudioNet\GraphQL\GraphQL; +use StudioNet\GraphQL\Transformer\Transformer; class GraphQLTest extends TestCase { use DatabaseTransactions; @@ -25,6 +26,6 @@ public function testGetSchemaException() { * @expectedException \StudioNet\GraphQL\Exception\TypeNotFoundException */ public function testRegisterTypeException() { - app(GraphQL::class)->registerType('\\Test\\Class\\Type'); + app(GraphQL::class)->registerType(null, '\\Test\\Class\\Type'); } } From a046610ec844c248e12df6e7ddc2b2c008f7689a Mon Sep 17 00:00:00 2001 From: Cyril Mizzi Date: Wed, 2 Aug 2017 12:21:46 +0200 Subject: [PATCH 10/11] fixes field transformer --- src/Transformer/FieldTransformer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Transformer/FieldTransformer.php b/src/Transformer/FieldTransformer.php index 8055397..46cc695 100644 --- a/src/Transformer/FieldTransformer.php +++ b/src/Transformer/FieldTransformer.php @@ -27,7 +27,7 @@ public function transform($instance) { if (method_exists($instance, 'getResolver')) { $attributes = $attributes + [ - 'resolve' => [$this, 'resolve'] + 'resolve' => [$instance, 'getResolver'] ]; } From 33c16c353652a66850b5efd6d268067aa32d970e Mon Sep 17 00:00:00 2001 From: Cyril Mizzi Date: Wed, 2 Aug 2017 12:22:21 +0200 Subject: [PATCH 11/11] removes Support\Field::toArray() method (handle by transformer) adds abstract keyword to Mutation and Query classes --- src/Support/Field.php | 21 --------------------- src/Support/FieldInterface.php | 3 +++ src/Support/Mutation.php | 8 +++++++- src/Support/Query.php | 8 +++++++- 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/Support/Field.php b/src/Support/Field.php index 554e291..e6d80e1 100644 --- a/src/Support/Field.php +++ b/src/Support/Field.php @@ -21,25 +21,4 @@ public function getAttributes() { public function getArguments() { return []; } - - /** - * Convert current field into array - * - * @return array - */ - public function toArray() { - $attributes = $this->getAttributes() + [ - 'type' => $this->getRelatedType(), - 'args' => $this->getArguments() - ]; - - // Assert we have a resolve method - if (method_exists($this, 'resolve')) { - $attributes = $attributes + [ - 'resolve' => [$this, 'resolve'] - ]; - } - - return $attributes; - } } diff --git a/src/Support/FieldInterface.php b/src/Support/FieldInterface.php index f996372..d44133f 100644 --- a/src/Support/FieldInterface.php +++ b/src/Support/FieldInterface.php @@ -1,6 +1,9 @@