├── .gitignore ├── Readme.md ├── composer.json ├── docs ├── advanced.md └── upgrade.md ├── src ├── Console │ ├── GeneratorCommand.php │ ├── MutationMakeCommand.php │ ├── PublishCommand.php │ ├── QueryMakeCommand.php │ ├── TypeMakeCommand.php │ └── stubs │ │ ├── mutation.stub │ │ ├── query.stub │ │ └── type.stub ├── Error │ └── ValidationError.php ├── Events │ ├── SchemaAdded.php │ └── TypeAdded.php ├── Exception │ ├── SchemaNotFound.php │ └── TypeNotFound.php ├── GraphQL.php ├── GraphQLController.php ├── ServiceProvider.php ├── Support │ ├── EnumType.php │ ├── Facades │ │ └── GraphQL.php │ ├── Field.php │ ├── InputObjectType.php │ ├── Instance.php │ ├── InterfaceType.php │ ├── Mutation.php │ ├── Query.php │ ├── ScalarType.php │ ├── Traits │ │ └── ShouldValidate.php │ └── Type.php ├── View │ └── GraphiQLComposer.php ├── config │ └── require │ │ └── graphql.php └── resources │ └── views │ └── graphiql.php └── tests ├── ConfigTest.php ├── EndpointTest.php ├── FieldTest.php ├── GraphQLQueryTest.php ├── GraphQLTest.php ├── GraphiQLTest.php ├── InterfaceTypeTest.php ├── MutationTest.php ├── Objects ├── CustomExampleType.php ├── ErrorFormatter.php ├── ExampleField.php ├── ExampleInterfaceType.php ├── ExampleType.php ├── ExampleValidationField.php ├── ExamplesContextQuery.php ├── ExamplesQuery.php ├── ExamplesRootQuery.php ├── UpdateExampleMutation.php ├── data.php └── queries.php ├── QueryTest.php ├── TestCase.php ├── TypeTest.php ├── fixture ├── app │ └── .gitignore └── composer.json └── logs └── .gitignore /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | *.log 3 | composer.lock 4 | /*_backup/ 5 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Yii2 GraphQL 2 | 3 | Данное расширение позволяет реализовать GraphQL API в Yii2. Расширение основано на [расширении GraphQL webonyx](https://github.com/webonyx/graphql-php). Более подробная информация о GraphQL - [GraphQL Introduction](http://facebook.github.io/react/blog/2015/05/01/graphql-introduction.html). 4 | 5 | 6 | ## Установка 7 | 8 | #### Зависимости: 9 | 10 | * [Базовое расширение GraphQL PHP](https://github.com/webonyx/graphql-php) 11 | * [Filsh/yii2-oauth2-server](https://github.com/Filsh/yii2-oauth2-server) 12 | 13 | 14 | **1-** Установка пакета через Composer. Добавьте следующий код в файл `composer.json`. 15 | ```json 16 | { 17 | "require": { 18 | "maxxxam/graphql": "~0.2" 19 | } 20 | } 21 | ``` 22 | 23 | **2-** Запустите установку пакетов Composer 24 | 25 | ```bash 26 | $ composer install 27 | ``` 28 | 29 | или обновите вместе с существующими пакетами 30 | 31 | ```bash 32 | $ composer update 33 | ``` 34 | 35 | ### Yii2 36 | 37 | **1-** Добавьте компонент в `config/main.php` file 38 | ```php 39 | 'graphql' => [ 40 | 'graphql' => require(__DIR__ . DIRECTORY_SEPARATOR . 'require' . DIRECTORY_SEPARATOR . 'graphql.php') 41 | ], 42 | ``` 43 | 44 | **2-** Переместите config `./src/config/require` в `common/config/require`. Отредактируйте конфигурацию согласно вашему окружению. 45 | Объекты Types, Queries, Mutations могут быть загружены вручную (заданием в конфигурации), либо автоматически из заданных директорий. 46 | Пример конфигурации `graphql.php` приведен ниже: 47 | ```php 48 | 49 | $appNamespace = 'app\modules\graphql'; 50 | $graphqlDir = dirname(dirname(__DIR__)) . '/modules/graphql'; 51 | 52 | return [ 53 | 'class' => 'GraphQLYii\GraphQL', 54 | 'namespace' => $appNamespace, 55 | 'graphqlDir' => $graphqlDir, 56 | 'types' => [ 57 | 'UserType' => $appNamespace . '\types\UserType', 58 | 'AccessType' => $appNamespace . '\types\AccessType', 59 | 'ImageType' => $appNamespace . '\types\ImageType', 60 | 'EventType' => $appNamespace . '\types\EventType', 61 | 'TrophyType' => $appNamespace . '\types\TrophyType', 62 | 'TeamType' => $appNamespace . '\types\TeamType', 63 | 'AlbumType' => $appNamespace . '\types\AlbumType', 64 | 'FriendType' => $appNamespace . '\types\FriendType', 65 | 'SportType' => $appNamespace . '\types\SportType', 66 | ], 67 | 'queries' => [ 68 | 'UsersQuery' => $appNamespace . '\query\user\UsersQuery', 69 | 'UserQuery' => $appNamespace . '\query\user\UserQuery' 70 | ], 71 | 'mutations' => [ 72 | 'AddFriendMutation' => $appNamespace . '\mutation\user\AddFriendMutation', 73 | 'RemoveFriendMutation' => $appNamespace . '\mutation\user\RemoveFriendMutation', 74 | 'SignupMutation' => $appNamespace . '\mutation\user\SignupMutation', 75 | ], 76 | 'typesPath' => '/types', 77 | 'queriesPath' => '/queries', 78 | 'mutationsPath' => '/mutations', 79 | 'subscriptionPath' => '/subscription', 80 | ]; 81 | ``` 82 | 83 | **3-** Создайте новый action для обработки пользовательского запроса 84 | 85 | ```php 86 | public function actionIndex(){ 87 | Yii::$app->response->format = Response::FORMAT_JSON; 88 | 89 | $data = Yii::$app->request->post(); 90 | 91 | $query = isset($data['query']) ? str_replace(["\r","\n"], "", $data['query']) : null; 92 | $params = isset($data['variables']) ? str_replace(["\r","\n"], "", $data['variables']) : null; 93 | 94 | /** @var GraphQL $GraphQL */ 95 | $GraphQL = \Yii::$app->get('graphql'); 96 | 97 | $result = $GraphQL->query($query, $params); 98 | 99 | if (!empty($result['errors'])){ 100 | Yii::$app->response->setStatusCode(400); 101 | } 102 | 103 | Yii::$app->response->headers->add('Content-Length', strlen(json_encode($result))); 104 | Yii::$app->response->headers->add('Content-Type', 'application/json'); 105 | 106 | $stream = fopen('php://memory','wb'); 107 | fwrite($stream, json_encode($result)); 108 | rewind($stream); 109 | 110 | Yii::$app->response->stream = $stream; 111 | Yii::$app->response->send(); 112 | 113 | return true; 114 | 115 | } 116 | ``` 117 | 118 | **4-** В качестве авторизации используется oAuth2 сервер [Filsh/yii2-oauth2-server](https://github.com/Filsh/yii2-oauth2-server) 119 | 120 | **5-** Добавьте поведение в Controller 121 | 122 | ```php 123 | public function behaviors() 124 | { 125 | Yii::$app->controller->enableCsrfValidation = false; 126 | return ArrayHelper::merge(parent::behaviors(), [ 127 | 'authenticator' => [ 128 | 'class' => CompositeAuth::className(), 129 | 'authMethods' => [ 130 | ['class' => HttpBearerAuth::className()], 131 | ['class' => QueryParamAuth::className(), 'tokenParam' => 'accessToken'], 132 | ] 133 | ], 134 | 'exceptionFilter' => [ 135 | 'class' => ErrorToExceptionFilter::className() 136 | ], 137 | ]); 138 | } 139 | ``` 140 | 141 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maxxxam/graphql-yii", 3 | "type": "library", 4 | "description": "GraphQL, yii2 lib", 5 | "keywords": ["GraphQL","yii2"], 6 | "homepage": "", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Maxim Belkanov", 11 | "email": "maxxxam-me@yandex.ru" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.5.9", 16 | "illuminate/support": "5.1.*|5.2.*|5.3.*|5.4.*", 17 | "webonyx/graphql-php": "0.9.*" 18 | }, 19 | "require-dev": { 20 | 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "GraphQLYii\\": "src/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "classmap": [ 29 | "tests/" 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/advanced.md: -------------------------------------------------------------------------------- 1 | # Advanced Usage 2 | 3 | - [Query variables](#query-variables) 4 | - [Custom field](#custom-field) 5 | - [Eager loading relationships](#eager-loading-relationships) 6 | 7 | ### Query Variables 8 | 9 | GraphQL offer you the possibility to use variables in your query so you don't need to "hardcode" value. This is done like that: 10 | 11 | ``` 12 | query FetchUserByID($id: String) { 13 | user(id: $id) { 14 | id 15 | email 16 | } 17 | } 18 | ``` 19 | 20 | When you query the GraphQL endpoint, you can pass a `params` parameter. 21 | 22 | ``` 23 | http://homestead.app/graphql?query=query+FetchUserByID($id:String){user(id:$id){id,email}}¶ms={"id":"1"} 24 | ``` 25 | 26 | ### Custom field 27 | 28 | You can also define a field as a class if you want to reuse it in multiple types. 29 | 30 | ```php 31 | 32 | namespace App\GraphQL\Fields; 33 | 34 | use GraphQL\Type\Definition\Type; 35 | use Folklore\GraphQL\Support\Field; 36 | 37 | class PictureField extends Field { 38 | 39 | protected $attributes = [ 40 | 'description' => 'A picture' 41 | ]; 42 | 43 | public function type(){ 44 | return Type::string(); 45 | } 46 | 47 | public function args() 48 | { 49 | return [ 50 | 'width' => [ 51 | 'type' => Type::int(), 52 | 'description' => 'The width of the picture' 53 | ], 54 | 'height' => [ 55 | 'type' => Type::int(), 56 | 'description' => 'The height of the picture' 57 | ] 58 | ]; 59 | } 60 | 61 | protected function resolve($root, $args) 62 | { 63 | $width = isset($args['width']) ? $args['width']:100; 64 | $height = isset($args['height']) ? $args['height']:100; 65 | return 'http://placehold.it/'.$width.'x'.$height; 66 | } 67 | 68 | } 69 | 70 | ``` 71 | 72 | You can then use it in your type declaration 73 | 74 | ```php 75 | 76 | namespace App\GraphQL\Type; 77 | 78 | use GraphQL\Type\Definition\Type; 79 | use Folklore\GraphQL\Support\Type as GraphQLType; 80 | 81 | use App\GraphQL\Fields\PictureField; 82 | 83 | class UserType extends GraphQLType { 84 | 85 | protected $attributes = [ 86 | 'name' => 'User', 87 | 'description' => 'A user' 88 | ]; 89 | 90 | public function fields() 91 | { 92 | return [ 93 | 'id' => [ 94 | 'type' => Type::nonNull(Type::string()), 95 | 'description' => 'The id of the user' 96 | ], 97 | 'email' => [ 98 | 'type' => Type::string(), 99 | 'description' => 'The email of user' 100 | ], 101 | //Instead of passing an array, you pass a class path to your custom field 102 | 'picture' => PictureField::class 103 | ]; 104 | } 105 | 106 | } 107 | 108 | ``` 109 | 110 | ### Eager loading relationships 111 | 112 | The third argument passed to a query's resolve method is an instance of `GraphQL\Type\Definition\ResolveInfo` which you can use to retrieve keys from the request. The following is an example of using this information to eager load related Eloquent models. 113 | 114 | Your Query would look like 115 | 116 | ```php 117 | namespace App\GraphQL\Query; 118 | 119 | use GraphQL; 120 | use GraphQL\Type\Definition\Type; 121 | use GraphQL\Type\Definition\ResolveInfo; 122 | use Folklore\GraphQL\Support\Query; 123 | 124 | use App\User; 125 | 126 | class UsersQuery extends Query 127 | { 128 | protected $attributes = [ 129 | 'name' => 'Users query' 130 | ]; 131 | 132 | public function type() 133 | { 134 | return Type::listOf(GraphQL::type('user')); 135 | } 136 | 137 | public function args() 138 | { 139 | return [ 140 | 'id' => ['name' => 'id', 'type' => Type::string()], 141 | 'email' => ['name' => 'email', 'type' => Type::string()] 142 | ]; 143 | } 144 | 145 | public function resolve($root, $args, $context, ResolveInfo $info) 146 | { 147 | $fields = $info->getFieldSelection($depth = 3); 148 | 149 | $users = User::query(); 150 | 151 | foreach ($fields as $field => $keys) { 152 | if ($field === 'profile') { 153 | $users->with('profile'); 154 | } 155 | 156 | if ($field === 'posts') { 157 | $users->with('posts'); 158 | } 159 | } 160 | 161 | return $users->get(); 162 | } 163 | } 164 | ``` 165 | 166 | Your Type for User would look like 167 | 168 | ```php 169 | 'User', 184 | 'description' => 'A user', 185 | ]; 186 | 187 | /** 188 | * @return array 189 | */ 190 | public function fields() 191 | { 192 | return [ 193 | 'uuid' => [ 194 | 'type' => Type::nonNull(Type::string()), 195 | 'description' => 'The uuid of the user' 196 | ], 197 | 'email' => [ 198 | 'type' => Type::nonNull(Type::string()), 199 | 'description' => 'The email of user' 200 | ], 201 | 'profile' => [ 202 | 'type' => GraphQL::type('Profile'), 203 | 'description' => 'The user profile', 204 | ], 205 | 'posts' => [ 206 | 'type' => Type::listOf(GraphQL::type('Post')), 207 | 'description' => 'The user posts', 208 | ] 209 | ]; 210 | } 211 | } 212 | 213 | ``` 214 | 215 | At this point we have a profile and a post type as expected for any model 216 | 217 | ```php 218 | class ProfileType extends GraphQLType 219 | { 220 | protected $attributes = [ 221 | 'name' => 'Profile', 222 | 'description' => 'A user profile', 223 | ]; 224 | 225 | public function fields() 226 | { 227 | return [ 228 | 'name' => [ 229 | 'type' => Type::string(), 230 | 'description' => 'The name of user' 231 | ] 232 | ]; 233 | } 234 | } 235 | ``` 236 | 237 | ```php 238 | class PostType extends GraphQLType 239 | { 240 | protected $attributes = [ 241 | 'name' => 'Post', 242 | 'description' => 'A post', 243 | ]; 244 | 245 | public function fields() 246 | { 247 | return [ 248 | 'title' => [ 249 | 'type' => Type::nonNull(Type::string()), 250 | 'description' => 'The title of the post' 251 | ], 252 | 'body' => [ 253 | 'type' => Type::string(), 254 | 'description' => 'The body the post' 255 | ] 256 | ]; 257 | } 258 | } 259 | ``` 260 | 261 | 262 | Lastly your query would look like, if using Homestead 263 | 264 | For example, if you use homestead: 265 | 266 | ``` 267 | http://homestead.app/graphql?query=query+FetchUsers{users{uuid, email, team{name}}} 268 | ``` 269 | -------------------------------------------------------------------------------- /docs/upgrade.md: -------------------------------------------------------------------------------- 1 | ## Upgrade to 1.0 2 | 3 | ### Multiple schemas 4 | The main difference between versions prior 1.0 and 1.0 is the use of multiple schemas. You will need to update your config to have the following structure: 5 | 6 | ```php 7 | 8 | 'schema' => 'default', 9 | 10 | 'schemas' => [ 11 | 'default' => [ 12 | 'query' => [ 13 | // Your queries 14 | ], 15 | 'mutation' => [ 16 | // Your mutations 17 | ] 18 | ] 19 | ] 20 | 21 | ``` 22 | 23 | ### Routes 24 | If you want to use routes that can accept schema name, you need to change `routes` to the following: 25 | 26 | ```php 27 | 28 | 'routes' => '{graphql_schema?}', 29 | 30 | // or if you use different routes for query and mutation 31 | 32 | 'routes' => [ 33 | 'query' => 'query/{graphql_schema?}', 34 | 'mutation' => 'mutation/{graphql_schema?}' 35 | ], 36 | 37 | ``` 38 | 39 | ### Facade methods 40 | The method `GraphQL::addQuery` and `GraphQL::addMutation` has been removed since it doesn't make sense with multiple schemas. You can use the new `GraphQL::addSchema` method to add new schemas. 41 | 42 | ### Context 43 | Since graphql-php v0.7 the arguments passed to the `resolve` method has changed. There is a third argument called `context`. 44 | 45 | ```php 46 | public function resolve($root, $args, $context, ResolveInfo $info) 47 | { 48 | 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /src/Console/GeneratorCommand.php: -------------------------------------------------------------------------------- 1 | replaceType($stub, $name); 63 | } 64 | 65 | /** 66 | * Replace the namespace for the given stub. 67 | * 68 | * @param string $stub 69 | * @param string $name 70 | * @return $this 71 | */ 72 | protected function replaceType($stub, $name) 73 | { 74 | preg_match('/([^\\\]+)$/', $name, $matches); 75 | $stub = str_replace( 76 | 'DummyMutation', 77 | $matches[1], 78 | $stub 79 | ); 80 | 81 | return $stub; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Console/PublishCommand.php: -------------------------------------------------------------------------------- 1 | destination) 33 | * 34 | * @var array 35 | */ 36 | protected $fileMap = []; 37 | 38 | public function __construct(Filesystem $files) 39 | { 40 | parent::__construct(); 41 | $this->files = $files; 42 | 43 | $fromPath = __DIR__ . '/../../..'; 44 | $this->fileMap = [ 45 | $fromPath.'/config/config.php' => app()->basePath('config/graphql.php'), 46 | $fromPath.'/resources/views/graphiql.php' => app()->basePath('resources/views/vendor/graphql/graphiql.php') 47 | ]; 48 | } 49 | 50 | /** 51 | * Execute the console command. 52 | * 53 | * @return mixed 54 | */ 55 | public function handle() 56 | { 57 | foreach ($this->fileMap as $from => $to) { 58 | if ($this->files->exists($to) && !$this->option('force')) { 59 | continue; 60 | } 61 | $this->createParentDirectory(dirname($to)); 62 | $this->files->copy($from, $to); 63 | $this->status($from, $to, 'File'); 64 | } 65 | } 66 | 67 | /** 68 | * Create the directory to house the published files if needed. 69 | * 70 | * @param string $directory 71 | * @return void 72 | */ 73 | protected function createParentDirectory($directory) 74 | { 75 | if (!$this->files->isDirectory($directory)) { 76 | $this->files->makeDirectory($directory, 0755, true); 77 | } 78 | } 79 | 80 | /** 81 | * Write a status message to the console. 82 | * 83 | * @param string $from 84 | * @param string $to 85 | * @return void 86 | */ 87 | protected function status($from, $to) 88 | { 89 | $from = str_replace(base_path(), '', realpath($from)); 90 | $to = str_replace(base_path(), '', realpath($to)); 91 | $this->line("Copied File [{$from}] To [{$to}]"); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/Console/QueryMakeCommand.php: -------------------------------------------------------------------------------- 1 | replaceType($stub, $name); 63 | } 64 | 65 | /** 66 | * Replace the namespace for the given stub. 67 | * 68 | * @param string $stub 69 | * @param string $name 70 | * @return $this 71 | */ 72 | protected function replaceType($stub, $name) 73 | { 74 | preg_match('/([^\\\]+)$/', $name, $matches); 75 | $stub = str_replace( 76 | 'DummyQuery', 77 | $matches[1], 78 | $stub 79 | ); 80 | 81 | return $stub; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Console/TypeMakeCommand.php: -------------------------------------------------------------------------------- 1 | replaceType($stub, $name); 63 | } 64 | 65 | /** 66 | * Replace the namespace for the given stub. 67 | * 68 | * @param string $stub 69 | * @param string $name 70 | * @return $this 71 | */ 72 | protected function replaceType($stub, $name) 73 | { 74 | preg_match('/([^\\\]+)$/', $name, $matches); 75 | $stub = str_replace( 76 | 'DummyType', 77 | $matches[1], 78 | $stub 79 | ); 80 | 81 | return $stub; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Console/stubs/mutation.stub: -------------------------------------------------------------------------------- 1 | 'DummyMutation', 14 | 'description' => 'A mutation' 15 | ]; 16 | 17 | public function type() 18 | { 19 | return Type::listOf(Type::string()); 20 | } 21 | 22 | public function args() 23 | { 24 | return [ 25 | 26 | ]; 27 | } 28 | 29 | public function resolve($root, $args, $context, ResolveInfo $info) 30 | { 31 | return []; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Console/stubs/query.stub: -------------------------------------------------------------------------------- 1 | 'DummyQuery', 14 | 'description' => 'A query' 15 | ]; 16 | 17 | public function type() 18 | { 19 | return Type::listOf(Type::string()); 20 | } 21 | 22 | public function args() 23 | { 24 | return [ 25 | 26 | ]; 27 | } 28 | 29 | public function resolve($root, $args, $context, ResolveInfo $info) 30 | { 31 | return []; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Console/stubs/type.stub: -------------------------------------------------------------------------------- 1 | 'DummyType', 13 | 'description' => 'A type' 14 | ]; 15 | 16 | public function fields() 17 | { 18 | return [ 19 | 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Error/ValidationError.php: -------------------------------------------------------------------------------- 1 | validator = $validator; 13 | 14 | return $this; 15 | } 16 | 17 | public function getValidator() 18 | { 19 | return $this->validator; 20 | } 21 | 22 | public function getValidatorMessages() 23 | { 24 | return $this->validator ? $this->validator->messages():[]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Events/SchemaAdded.php: -------------------------------------------------------------------------------- 1 | schema = $schema; 11 | $this->name = $name; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Events/TypeAdded.php: -------------------------------------------------------------------------------- 1 | type = $type; 11 | $this->name = $name; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Exception/SchemaNotFound.php: -------------------------------------------------------------------------------- 1 | clearTypeInstances(); 52 | 53 | if (!empty($this->schema)){ 54 | try{ 55 | $schema = new $this->schema; 56 | $this->schema = $schema::build(); 57 | }catch(\Exception $error){ 58 | throw new SchemaNotFound('Type ' . $this->schema . ' not found.'); 59 | } 60 | } 61 | 62 | /** Collect objects from config and files directory */ 63 | $this->types = array_unique( array_merge($this->types, $this->getListFiles($this->graphqlDir, $this->typesPath)) ); 64 | $this->queries = array_unique( array_merge($this->queries, $this->getListFiles($this->graphqlDir, $this->queriesPath)) ); 65 | $this->mutations = array_unique( array_merge($this->mutations, $this->getListFiles($this->graphqlDir, $this->mutationsPath)) ); 66 | 67 | /** Collect all types */ 68 | $types = []; 69 | foreach ($this->types as $name => $type) { 70 | $newType = $this->type($name); 71 | if ($newType) { 72 | $types[] = $newType; 73 | } 74 | } 75 | 76 | $this->queryInstance = $this->objectType($this->queries, [ 77 | 'name' => 'Query' 78 | ]); 79 | 80 | $this->mutationInstance = $this->objectType($this->mutations, [ 81 | 'name' => 'Mutation' 82 | ]); 83 | 84 | return new Schema([ 85 | 'query' => $this->queryInstance, 86 | 'mutation' => $this->mutationInstance, 87 | 'types' => $types 88 | ]); 89 | } 90 | 91 | /** 92 | * Получение объектов из директории 93 | * @param $basepath 94 | * @param string $subPath 95 | * @return array 96 | */ 97 | private function getListFiles($basepath, $subPath = ''){ 98 | $files = []; 99 | $subNamespace = str_replace('/', '\\', $subPath); 100 | $path = $basepath . $subPath; 101 | if ($path !== ''){ 102 | if (file_exists($path)) { 103 | $fp = opendir($path); 104 | while ($cvFile = readdir($fp)) { 105 | $fileName = $path . '/' . $cvFile; 106 | if (is_file($fileName)) { 107 | if (preg_match('/(.*)\.php/', $cvFile, $matches)) { 108 | $files[$matches[1]] = $this->namespace . $subNamespace . '\\' . $matches[1]; 109 | } 110 | } elseif (!in_array($cvFile, ['.', '..'], true) && is_dir($fileName)) { 111 | $files = array_merge($files, $this->getListFiles($basepath, $subPath . '/' . $cvFile)); 112 | } 113 | } 114 | closedir($fp); 115 | } 116 | } 117 | return $files; 118 | } 119 | 120 | public function type($name, $fresh = false) 121 | { 122 | if (!isset($this->types[$name])) { 123 | throw new TypeNotFound('Type '. $name.' not found.'); 124 | } 125 | 126 | if (!$fresh && isset($this->typesInstances[$name])) { 127 | return $this->typesInstances[$name]; 128 | } 129 | 130 | $class = $this->types[$name]; 131 | 132 | $type = $this->objectType($class, [ 133 | 'name' => $name 134 | ]); 135 | 136 | if ($type) { 137 | $this->typesInstances[$name] = $type; 138 | } 139 | 140 | return $type; 141 | } 142 | 143 | /** 144 | * Uses if Query is field of anyone types 145 | * @param $name 146 | * @param bool $fresh 147 | * @return array 148 | */ 149 | public function queryAsField($name) 150 | { 151 | if (!empty($this->queryInstance)) { 152 | $queryInstance = $this->queryInstance->getField($name); 153 | return [ 154 | 'type' => $queryInstance->getType(), 155 | 'args' => $queryInstance->config['args'], 156 | 'resolve' => $queryInstance->resolveFn 157 | ]; 158 | } 159 | } 160 | 161 | public function objectType($type, $opts = []) 162 | { 163 | // If it's already an ObjectType, just update properties and return it. 164 | // If it's an array, assume it's an array of fields and build ObjectType 165 | // from it. Otherwise, build it from a string or an instance. 166 | $objectType = null; 167 | if ($type instanceof ObjectType) { 168 | $objectType = $type; 169 | foreach ($opts as $key => $value) { 170 | if (property_exists($objectType, $key)) { 171 | $objectType->{$key} = $value; 172 | } 173 | if (isset($objectType->config[$key])) { 174 | $objectType->config[$key] = $value; 175 | } 176 | } 177 | } elseif (is_array($type)) { 178 | $objectType = $this->buildObjectTypeFromFields($type, $opts); 179 | } else { 180 | $objectType = $this->buildObjectTypeFromClass($type, $opts); 181 | } 182 | 183 | return $objectType; 184 | } 185 | 186 | public function query($query, $params = [], $opts = []) 187 | { 188 | $result = $this->queryAndReturnResult($query, $params, $opts); 189 | 190 | if (!empty($result->errors)) { 191 | return [ 192 | 'data' => $result->data, 193 | 'errors' => array_map([$this, 'formatError'], $result->errors) 194 | ]; 195 | } 196 | 197 | return [ 198 | 'data' => $result->data 199 | ]; 200 | 201 | } 202 | 203 | public function queryAndReturnResult($query, $params = [], $opts = []) 204 | { 205 | $root = array_get($opts, 'root', null); 206 | $context = array_get($opts, 'context', null); 207 | $schemaName = array_get($opts, 'schema', null); 208 | $operationName = array_get($opts, 'operationName', null); 209 | 210 | $schema = $this->schema($schemaName); 211 | 212 | $result = GraphQLBase::executeAndReturnResult($schema, $query, $root, $context, $params, $operationName); 213 | 214 | return $result; 215 | } 216 | 217 | public function addTypes($types) 218 | { 219 | foreach ($types as $name => $type) { 220 | $this->addType($type, is_numeric($name) ? null:$name); 221 | } 222 | } 223 | 224 | public function addType($class, $name = null) 225 | { 226 | $name = $this->getTypeName($class, $name); 227 | $this->types[$name] = $class; 228 | } 229 | 230 | public function addSchema($name, $schema) 231 | { 232 | $this->schemas[$name] = $schema; 233 | } 234 | 235 | public function clearType($name) 236 | { 237 | if (isset($this->types[$name])) { 238 | unset($this->types[$name]); 239 | } 240 | } 241 | 242 | public function clearSchema($name) 243 | { 244 | if (isset($this->schemas[$name])) { 245 | unset($this->schemas[$name]); 246 | } 247 | } 248 | 249 | public function clearTypes() 250 | { 251 | $this->types = []; 252 | } 253 | 254 | public function clearSchemas() 255 | { 256 | $this->schemas = []; 257 | } 258 | 259 | public function getTypes() 260 | { 261 | return $this->types; 262 | } 263 | 264 | public function getSchemas() 265 | { 266 | return $this->schemas; 267 | } 268 | 269 | protected function clearTypeInstances() 270 | { 271 | $this->typesInstances = []; 272 | } 273 | 274 | protected function buildObjectTypeFromClass($type, $opts = []) 275 | { 276 | if (!is_object($type)) { 277 | try { 278 | $type = new $type($opts); 279 | } catch (\Error $e){ 280 | return false; 281 | } 282 | } 283 | 284 | foreach ($opts as $key => $value) { 285 | $type->{$key} = $value; 286 | } 287 | 288 | return $type->toType(); 289 | } 290 | 291 | protected function buildObjectTypeFromFields($fields, $opts = []) 292 | { 293 | $typeFields = []; 294 | foreach ($fields as $name => $field) { 295 | if (is_string($field)) { 296 | try { 297 | $field = new $field; 298 | } catch (\Error $e){ 299 | continue; 300 | } 301 | $name = is_numeric($name) ? $field->name:$name; 302 | $field->name = $name; 303 | $field = $field->toArray(); 304 | } else { 305 | $name = is_numeric($name) ? $field['name']:$name; 306 | $field['name'] = $name; 307 | } 308 | $typeFields[$name] = $field; 309 | } 310 | 311 | return new ObjectType(array_merge([ 312 | 'fields' => $typeFields 313 | ], $opts)); 314 | } 315 | 316 | protected function getTypeName($class, $name = null) 317 | { 318 | if ($name) { 319 | return $name; 320 | } 321 | 322 | $type = is_object($class) ? $class : new $class; 323 | return $type->name; 324 | } 325 | 326 | public static function formatError($e) 327 | { 328 | $error = [ 329 | 'message' => $e->getMessage() 330 | ]; 331 | 332 | $locations = $e->getLocations(); 333 | if (!empty($locations)) { 334 | $error['locations'] = array_map(function ($loc) { 335 | return $loc->toArray(); 336 | }, $locations); 337 | } 338 | 339 | $previous = $e->getPrevious(); 340 | if (!empty($previous->statusCode)) { 341 | $error['statusCode'] = $previous->statusCode; 342 | } 343 | 344 | $previous = $e->getPrevious(); 345 | if (!empty($previous)) { 346 | $error['file'] = $previous->getFile(); 347 | $error['line'] = $previous->getLine(); 348 | } 349 | 350 | $stackTrace = $e->getTrace(); 351 | if (!empty($stackTrace)) { 352 | foreach ($stackTrace as $key => $item) { 353 | $newItem = []; 354 | $newItem['file'] = isset($item['file']) ? $item['file'] : ''; 355 | $newItem['line'] = isset($item['line']) ? $item['line'] : ''; 356 | $newItem['function'] = isset($item['function']) ? $item['function'] : ''; 357 | $error['trace'][] = $newItem; 358 | } 359 | } 360 | 361 | if ($previous && $previous instanceof ValidationError) { 362 | $error['validation'] = $previous->getValidatorMessages(); 363 | } 364 | 365 | return $error; 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /src/GraphQLController.php: -------------------------------------------------------------------------------- 1 | response->format = Response::FORMAT_JSON; 25 | 26 | $data = Yii::$app->request->post(); 27 | 28 | $query = isset($data['query']) ? str_replace(["\r","\n"], "", $data['query']) : null; 29 | $params = isset($data['variables']) ? str_replace(["\r","\n"], "", $data['variables']) : null; 30 | 31 | /** @var GraphQL $GraphQL */ 32 | $GraphQL = \Yii::$app->get('graphql'); 33 | 34 | $result = $GraphQL->query($query, $params); 35 | 36 | if (!empty($result['errors'])){ 37 | Yii::$app->response->setStatusCode(400); 38 | } 39 | 40 | Yii::$app->response->headers->add('Content-Length', strlen(json_encode($result))); 41 | Yii::$app->response->headers->add('Content-Type', 'application/json'); 42 | 43 | $stream = fopen('php://memory','wb'); 44 | fwrite($stream, json_encode($result)); 45 | rewind($stream); 46 | 47 | Yii::$app->response->stream = $stream; 48 | Yii::$app->response->send(); 49 | 50 | return true; 51 | 52 | } 53 | 54 | public function behaviors() 55 | { 56 | Yii::$app->controller->enableCsrfValidation = false; 57 | return ArrayHelper::merge(parent::behaviors(), [ 58 | 'authenticator' => [ 59 | 'class' => CompositeAuth::className(), 60 | 'authMethods' => [ 61 | ['class' => HttpBearerAuth::className()], 62 | ['class' => QueryParamAuth::className(), 'tokenParam' => 'accessToken'], 63 | ] 64 | ], 65 | 'exceptionFilter' => [ 66 | 'class' => ErrorToExceptionFilter::className() 67 | ], 68 | ]); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | app['router']; 16 | } 17 | 18 | /** 19 | * Bootstrap any application services. 20 | * 21 | * @return void 22 | */ 23 | public function boot() 24 | { 25 | $this->bootEvents(); 26 | 27 | $this->bootPublishes(); 28 | 29 | $this->bootTypes(); 30 | 31 | $this->bootSchemas(); 32 | 33 | $this->bootRouter(); 34 | 35 | $this->bootViews(); 36 | 37 | $this->bootSecurity(); 38 | } 39 | 40 | /** 41 | * Bootstrap router 42 | * 43 | * @return void 44 | */ 45 | protected function bootRouter() 46 | { 47 | if (config('graphql.routes')) { 48 | $router = $this->getRouter(); 49 | include __DIR__.'/routes.php'; 50 | } 51 | } 52 | 53 | /** 54 | * Bootstrap events 55 | * 56 | * @return void 57 | */ 58 | protected function bootEvents() 59 | { 60 | //Update the schema route pattern when schema is added 61 | $this->app['events']->listen(\Folklore\GraphQL\Events\SchemaAdded::class, function () { 62 | $schemaNames = array_keys($this->app['graphql']->getSchemas()); 63 | $this->getRouter()->pattern('graphql_schema', '('.implode('|', $schemaNames).')'); 64 | }); 65 | } 66 | 67 | /** 68 | * Bootstrap publishes 69 | * 70 | * @return void 71 | */ 72 | protected function bootPublishes() 73 | { 74 | $configPath = __DIR__.'/../../config'; 75 | $viewsPath = __DIR__.'/../../resources/views'; 76 | 77 | $this->mergeConfigFrom($configPath.'/config.php', 'graphql'); 78 | 79 | $this->loadViewsFrom($viewsPath, 'graphql'); 80 | 81 | $this->publishes([ 82 | $configPath.'/config.php' => config_path('graphql.php'), 83 | ], 'config'); 84 | 85 | $this->publishes([ 86 | $viewsPath => base_path('resources/views/vendor/graphql'), 87 | ], 'views'); 88 | } 89 | 90 | /** 91 | * Add types from config 92 | * 93 | * @return void 94 | */ 95 | protected function bootTypes() 96 | { 97 | $configTypes = config('graphql.types'); 98 | foreach ($configTypes as $name => $type) { 99 | if (is_numeric($name)) { 100 | $this->app['graphql']->addType($type); 101 | } else { 102 | $this->app['graphql']->addType($type, $name); 103 | } 104 | } 105 | } 106 | 107 | /** 108 | * Add schemas from config 109 | * 110 | * @return void 111 | */ 112 | protected function bootSchemas() 113 | { 114 | $configSchemas = config('graphql.schemas'); 115 | foreach ($configSchemas as $name => $schema) { 116 | $this->app['graphql']->addSchema($name, $schema); 117 | } 118 | } 119 | 120 | /** 121 | * Bootstrap Views 122 | * 123 | * @return void 124 | */ 125 | protected function bootViews() 126 | { 127 | $graphiQL = config('graphql.graphiql', true); 128 | if ($graphiQL) { 129 | $view = config('graphql.graphiql.view', 'graphql::graphiql'); 130 | app('view')->composer($view, \Folklore\GraphQL\View\GraphiQLComposer::class); 131 | } 132 | } 133 | 134 | /** 135 | * Configure security from config 136 | * @return void 137 | */ 138 | protected function bootSecurity() 139 | { 140 | $maxQueryComplexity = config('graphql.security.query_max_complexity'); 141 | if ($maxQueryComplexity !== null) { 142 | $queryComplexity = DocumentValidator::getRule('QueryComplexity'); 143 | $queryComplexity->setMaxQueryComplexity($maxQueryComplexity); 144 | } 145 | 146 | $maxQueryDepth = config('graphql.security.query_max_depth'); 147 | if ($maxQueryDepth !== null) { 148 | $queryDepth = DocumentValidator::getRule('QueryDepth'); 149 | $queryDepth->setMaxQueryDepth($maxQueryDepth); 150 | } 151 | } 152 | 153 | /** 154 | * Register any application services. 155 | * 156 | * @return void 157 | */ 158 | public function register() 159 | { 160 | $this->registerGraphQL(); 161 | 162 | $this->registerConsole(); 163 | } 164 | 165 | /** 166 | * Register GraphQL facade 167 | * 168 | * @return void 169 | */ 170 | public function registerGraphQL() 171 | { 172 | $this->app->singleton('graphql', function ($app) { 173 | return new GraphQL($app); 174 | }); 175 | } 176 | 177 | /** 178 | * Register console commands 179 | * 180 | * @return void 181 | */ 182 | public function registerConsole() 183 | { 184 | $this->commands(\Folklore\GraphQL\Console\TypeMakeCommand::class); 185 | $this->commands(\Folklore\GraphQL\Console\QueryMakeCommand::class); 186 | $this->commands(\Folklore\GraphQL\Console\MutationMakeCommand::class); 187 | } 188 | 189 | 190 | /** 191 | * Get the services provided by the provider. 192 | * 193 | * @return array 194 | */ 195 | public function provides() 196 | { 197 | return ['graphql']; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/Support/EnumType.php: -------------------------------------------------------------------------------- 1 | getAttributes(), [ 31 | 'values' => $this->values() 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Support/Facades/GraphQL.php: -------------------------------------------------------------------------------- 1 | appInstance = Instance::getAppInstance(); 16 | parent::__construct($attributes); 17 | } 18 | 19 | public function attributes() 20 | { 21 | return []; 22 | } 23 | 24 | public function type() 25 | { 26 | return null; 27 | } 28 | 29 | public function args() 30 | { 31 | return []; 32 | } 33 | 34 | protected function getResolver() 35 | { 36 | if (!method_exists($this, 'resolve')) { 37 | return null; 38 | } 39 | 40 | $resolver = array($this, 'resolve'); 41 | return function () use ($resolver) { 42 | $args = func_get_args(); 43 | return call_user_func_array($resolver, $args); 44 | }; 45 | } 46 | 47 | /** 48 | * Get the attributes from the container. 49 | * 50 | * @return array 51 | */ 52 | public function getAttributes() 53 | { 54 | $attributes = $this->attributes(); 55 | $args = $this->args(); 56 | 57 | $attributes = array_merge($this->attributes, [ 58 | 'args' => $args 59 | ], $attributes); 60 | 61 | $type = $this->type(); 62 | if (isset($type)) { 63 | $attributes['type'] = $type; 64 | } 65 | 66 | $resolver = $this->getResolver(); 67 | if (isset($resolver)) { 68 | $attributes['resolve'] = $resolver; 69 | } 70 | 71 | return $attributes; 72 | } 73 | 74 | /** 75 | * Convert the Fluent instance to an array. 76 | * 77 | * @return array 78 | */ 79 | public function toArray() 80 | { 81 | return $this->getAttributes(); 82 | } 83 | 84 | /** 85 | * Dynamically retrieve the value of an attribute. 86 | * 87 | * @param string $key 88 | * @return mixed 89 | */ 90 | public function __get($key) 91 | { 92 | $attributes = $this->getAttributes(); 93 | return isset($attributes[$key]) ? $attributes[$key]:null; 94 | } 95 | 96 | /** 97 | * Dynamically check if an attribute is set. 98 | * 99 | * @param string $key 100 | * @return void 101 | */ 102 | public function __isset($key) 103 | { 104 | $attributes = $this->getAttributes(); 105 | return isset($attributes[$key]); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Support/InputObjectType.php: -------------------------------------------------------------------------------- 1 | appInstance = Instance::getAppInstance(); 21 | parent::__construct($attributes); 22 | } 23 | 24 | /** 25 | * @return mixed 26 | */ 27 | public static function type(){ 28 | $array = explode('\\', static::class); 29 | $className = $array[count($array) - 1]; 30 | return Instance::getAppInstance()->type($className); 31 | } 32 | 33 | public function attributes() 34 | { 35 | return []; 36 | } 37 | 38 | public function fields() 39 | { 40 | return []; 41 | } 42 | 43 | public function interfaces() 44 | { 45 | return []; 46 | } 47 | 48 | protected function getFieldResolver($name, $field) 49 | { 50 | $resolveMethod = 'resolve'.studly_case($name).'Field'; 51 | if (isset($field['resolve'])) { 52 | return $field['resolve']; 53 | } elseif (method_exists($this, $resolveMethod)) { 54 | $resolver = array($this, $resolveMethod); 55 | return function () use ($resolver) { 56 | $args = func_get_args(); 57 | return call_user_func_array($resolver, $args); 58 | }; 59 | } 60 | 61 | return null; 62 | } 63 | 64 | public function getFields() 65 | { 66 | $fields = $this->fields(); 67 | $allFields = []; 68 | foreach ($fields as $name => $field) { 69 | if (is_string($field)) { 70 | $field = app($field); 71 | $field->name = $name; 72 | $allFields[$name] = $field->toArray(); 73 | } else { 74 | $resolver = $this->getFieldResolver($name, $field); 75 | if ($resolver) { 76 | $field['resolve'] = $resolver; 77 | } 78 | $allFields[$name] = $field; 79 | } 80 | } 81 | 82 | return $allFields; 83 | } 84 | 85 | /** 86 | * Get the attributes from the container. 87 | * 88 | * @return array 89 | */ 90 | public function getAttributes() 91 | { 92 | $attributes = $this->attributes(); 93 | $interfaces = $this->interfaces(); 94 | 95 | $attributes = array_merge($this->attributes, [ 96 | 'fields' => function () { 97 | return $this->getFields(); 98 | } 99 | ], $attributes); 100 | 101 | if (count($interfaces)) { 102 | $attributes['interfaces'] = $interfaces; 103 | } 104 | 105 | return $attributes; 106 | } 107 | 108 | /** 109 | * Convert the Fluent instance to an array. 110 | * 111 | * @return array 112 | */ 113 | public function toArray() 114 | { 115 | return $this->getAttributes(); 116 | } 117 | 118 | public function toType() 119 | { 120 | return new GQObject($this->toArray()); 121 | } 122 | 123 | 124 | 125 | } 126 | -------------------------------------------------------------------------------- /src/Support/Instance.php: -------------------------------------------------------------------------------- 1 | getTypeResolver(); 33 | if (isset($resolver)) { 34 | $attributes['resolveType'] = $resolver; 35 | } 36 | 37 | return $attributes; 38 | } 39 | 40 | public function toType() 41 | { 42 | return new BaseInterfaceType($this->toArray()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Support/Mutation.php: -------------------------------------------------------------------------------- 1 | queryAsField($className); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/Support/ScalarType.php: -------------------------------------------------------------------------------- 1 | getAttributes(), [ 49 | 'serialize' => [$this, 'serialize'], 50 | 'parseValue' => [$this, 'parseValue'], 51 | 'parseLiteral' => [$this, 'parseLiteral'], 52 | ]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Support/Traits/ShouldValidate.php: -------------------------------------------------------------------------------- 1 | args() as $name => $arg) { 21 | if (isset($arg['rules'])) { 22 | if (is_callable($arg['rules'])) { 23 | $argsRules[$name] = call_user_func_array($arg['rules'], $arguments); 24 | } else { 25 | $argsRules[$name] = $arg['rules']; 26 | } 27 | } 28 | } 29 | 30 | return array_merge($rules, $argsRules); 31 | } 32 | 33 | protected function getValidator($args, $rules) 34 | { 35 | return app('validator')->make($args, $rules); 36 | } 37 | 38 | protected function getResolver() 39 | { 40 | $resolver = parent::getResolver(); 41 | if (!$resolver) { 42 | return null; 43 | } 44 | 45 | return function () use ($resolver) { 46 | $arguments = func_get_args(); 47 | 48 | $rules = call_user_func_array([$this, 'getRules'], $arguments); 49 | if (sizeof($rules)) { 50 | $args = array_get($arguments, 1, []); 51 | $validator = $this->getValidator($args, $rules); 52 | if ($validator->fails()) { 53 | throw with(new ValidationError('validation'))->setValidator($validator); 54 | } 55 | } 56 | 57 | return call_user_func_array($resolver, $arguments); 58 | }; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Support/Type.php: -------------------------------------------------------------------------------- 1 | appInstance = Instance::getAppInstance(); 27 | parent::__construct($attributes); 28 | } 29 | 30 | public function attributes() 31 | { 32 | return []; 33 | } 34 | 35 | public function fields() 36 | { 37 | return []; 38 | } 39 | 40 | public function interfaces() 41 | { 42 | return []; 43 | } 44 | 45 | /** 46 | * @return mixed 47 | */ 48 | public static function type(){ 49 | $array = explode('\\', static::class); 50 | $className = $array[count($array) - 1]; 51 | return Instance::getAppInstance()->type($className); 52 | } 53 | 54 | protected function getFieldResolver($name, $field) 55 | { 56 | $resolveMethod = 'resolve'.studly_case($name).'Field'; 57 | if (isset($field['resolve'])) { 58 | return $field['resolve']; 59 | } 60 | 61 | if (method_exists($this, $resolveMethod)) { 62 | $resolver = array($this, $resolveMethod); 63 | return function () use ($resolver) { 64 | $args = func_get_args(); 65 | return call_user_func_array($resolver, $args); 66 | }; 67 | } 68 | 69 | return null; 70 | } 71 | 72 | public function getFields() 73 | { 74 | $fields = $this->fields(); 75 | $allFields = []; 76 | foreach ($fields as $name => $field) { 77 | if (is_string($field)) { 78 | $field = app($field); 79 | $field->name = $name; 80 | $allFields[$name] = $field->toArray(); 81 | } else { 82 | $resolver = $this->getFieldResolver($name, $field); 83 | if ($resolver) { 84 | $field['resolve'] = $resolver; 85 | } 86 | $allFields[$name] = $field; 87 | } 88 | } 89 | 90 | return $allFields; 91 | } 92 | 93 | /** 94 | * Get the attributes from the container. 95 | * 96 | * @return array 97 | */ 98 | public function getAttributes() 99 | { 100 | $attributes = $this->attributes(); 101 | $interfaces = $this->interfaces(); 102 | 103 | $attributes = array_merge($this->attributes, [ 104 | 'fields' => function () { 105 | return $this->getFields(); 106 | } 107 | ], $attributes); 108 | 109 | if (count($interfaces)) { 110 | $attributes['interfaces'] = $interfaces; 111 | } 112 | 113 | return $attributes; 114 | } 115 | 116 | /** 117 | * Convert the Fluent instance to an array. 118 | * 119 | * @return array 120 | */ 121 | public function toArray() 122 | { 123 | return $this->getAttributes(); 124 | } 125 | 126 | public function toType() 127 | { 128 | if ($this->inputObject) { 129 | return new InputObjectType($this->toArray()); 130 | } 131 | 132 | if ($this->scalarType) { 133 | return new CustomScalarType($this->toArray()); 134 | } 135 | 136 | if ($this->enumType) { 137 | return new EnumType($this->toArray()); 138 | } 139 | 140 | return new ObjectType($this->toArray()); 141 | } 142 | 143 | /** 144 | * Dynamically retrieve the value of an attribute. 145 | * 146 | * @param string $key 147 | * @return mixed 148 | */ 149 | public function __get($key) 150 | { 151 | $attributes = $this->getAttributes(); 152 | return isset($attributes[$key]) ? $attributes[$key]:null; 153 | } 154 | 155 | /** 156 | * Dynamically check if an attribute is set. 157 | * 158 | * @param string $key 159 | * @return void 160 | */ 161 | public function __isset($key) 162 | { 163 | $attributes = $this->getAttributes(); 164 | return isset($attributes[$key]); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/View/GraphiQLComposer.php: -------------------------------------------------------------------------------- 1 | graphqlPath = route('graphql.query'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/config/require/graphql.php: -------------------------------------------------------------------------------- 1 | 'GraphQLYii\GraphQL', 8 | 'namespace' => $appNamespace, 9 | 'graphqlDir' => $graphqlDir, 10 | /* 'types' => [ 11 | 'UserType' => $appNamespace . '\types\UserType', 12 | 'AccessType' => $appNamespace . '\types\AccessType', 13 | 'ImageType' => $appNamespace . '\types\ImageType', 14 | 'EventType' => $appNamespace . '\types\EventType', 15 | 'TrophyType' => $appNamespace . '\types\TrophyType', 16 | 'TeamType' => $appNamespace . '\types\TeamType', 17 | 'AlbumType' => $appNamespace . '\types\AlbumType', 18 | 'FriendType' => $appNamespace . '\types\FriendType', 19 | 'SportType' => $appNamespace . '\types\SportType', 20 | ], 21 | 'queries' => [ 22 | 'UsersQuery' => $appNamespace . '\query\user\UsersQuery', 23 | 'UserQuery' => $appNamespace . '\query\user\UserQuery' 24 | ], 25 | 'mutations' => [ 26 | 'AddFriendMutation' => $appNamespace . '\mutation\user\AddFriendMutation', 27 | 'RemoveFriendMutation' => $appNamespace . '\mutation\user\RemoveFriendMutation', 28 | 'SignupMutation' => $appNamespace . '\mutation\user\SignupMutation', 29 | ], */ 30 | 'typesPath' => '/types', 31 | 'queriesPath' => '/queries', 32 | 'mutationsPath' => '/mutations', 33 | 'subscriptionPath' => '/subscription', 34 | ]; 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/resources/views/graphiql.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
Loading...
23 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /tests/ConfigTest.php: -------------------------------------------------------------------------------- 1 | set('graphql', [ 12 | 13 | 'prefix' => 'graphql_test', 14 | 15 | 'routes' => [ 16 | 'query' => 'query/{graphql_schema?}', 17 | 'mutation' => 'mutation/{graphql_schema?}' 18 | ], 19 | 20 | 'variables_input_name' => 'variables', 21 | 22 | 'schema' => 'custom', 23 | 24 | 'schemas' => [ 25 | 'default' => [ 26 | 'query' => [ 27 | 'examples' => ExamplesQuery::class, 28 | 'examplesContext' => ExamplesContextQuery::class, 29 | 'examplesRoot' => ExamplesRootQuery::class 30 | ], 31 | 'mutation' => [ 32 | 'updateExample' => UpdateExampleMutation::class 33 | ] 34 | ], 35 | 'custom' => [ 36 | 'query' => [ 37 | 'examplesCustom' => ExamplesQuery::class 38 | ], 39 | 'mutation' => [ 40 | 'updateExampleCustom' => UpdateExampleMutation::class 41 | ] 42 | ] 43 | ], 44 | 45 | 'types' => [ 46 | 'Example' => ExampleType::class, 47 | CustomExampleType::class 48 | ], 49 | 50 | 'security' => [ 51 | 'query_max_complexity' => 1000, 52 | 'query_max_depth' => 10, 53 | ], 54 | 55 | ]); 56 | } 57 | 58 | public function testRouteQuery() 59 | { 60 | $response = $this->call('GET', '/graphql_test/query', [ 61 | 'query' => $this->queries['examplesCustom'] 62 | ]); 63 | 64 | $this->assertEquals($response->getStatusCode(), 200); 65 | 66 | $content = $response->getOriginalContent(); 67 | $this->assertArrayHasKey('data', $content); 68 | } 69 | 70 | public function testRouteMutation() 71 | { 72 | $response = $this->call('POST', '/graphql_test/mutation', [ 73 | 'query' => $this->queries['updateExampleCustom'] 74 | ]); 75 | 76 | $this->assertEquals($response->getStatusCode(), 200); 77 | 78 | $content = $response->getOriginalContent(); 79 | $this->assertArrayHasKey('data', $content); 80 | } 81 | 82 | public function testTypes() 83 | { 84 | $types = GraphQL::getTypes(); 85 | $this->assertArrayHasKey('Example', $types); 86 | $this->assertArrayHasKey('CustomExample', $types); 87 | } 88 | 89 | public function testSchema() 90 | { 91 | $schema = GraphQL::schema(); 92 | $schemaCustom = GraphQL::schema('custom'); 93 | 94 | $this->assertEquals($schema, $schemaCustom); 95 | } 96 | 97 | public function testSchemas() 98 | { 99 | $schemas = GraphQL::getSchemas(); 100 | 101 | $this->assertArrayHasKey('default', $schemas); 102 | $this->assertArrayHasKey('custom', $schemas); 103 | } 104 | 105 | public function testVariablesInputName() 106 | { 107 | $response = $this->call('GET', '/graphql_test/query/default', [ 108 | 'query' => $this->queries['examplesWithParams'], 109 | 'variables' => [ 110 | 'index' => 0 111 | ] 112 | ]); 113 | 114 | $this->assertEquals($response->getStatusCode(), 200); 115 | 116 | $content = $response->getOriginalContent(); 117 | $this->assertArrayHasKey('data', $content); 118 | $this->assertEquals($content['data'], [ 119 | 'examples' => [ 120 | $this->data[0] 121 | ] 122 | ]); 123 | } 124 | 125 | public function testSecurity() 126 | { 127 | $queryComplexity = DocumentValidator::getRule('QueryComplexity'); 128 | $this->assertEquals(1000, $queryComplexity->getMaxQueryComplexity()); 129 | 130 | $queryDepth = DocumentValidator::getRule('QueryDepth'); 131 | $this->assertEquals(10, $queryDepth->getMaxQueryDepth()); 132 | } 133 | 134 | public function testErrorFormatter() 135 | { 136 | $error = $this->getMockBuilder(ErrorFormatter::class) 137 | ->setMethods(['formatError']) 138 | ->getMock(); 139 | 140 | $error->expects($this->once()) 141 | ->method('formatError'); 142 | 143 | config([ 144 | 'graphql.error_formatter' => [$error, 'formatError'] 145 | ]); 146 | 147 | $result = GraphQL::query($this->queries['examplesWithError']); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tests/EndpointTest.php: -------------------------------------------------------------------------------- 1 | call('GET', '/graphql', [ 16 | 'query' => $this->queries['examples'] 17 | ]); 18 | 19 | $this->assertEquals($response->getStatusCode(), 200); 20 | 21 | $content = $response->getOriginalContent(); 22 | $this->assertArrayHasKey('data', $content); 23 | $this->assertEquals($content['data'], [ 24 | 'examples' => $this->data 25 | ]); 26 | } 27 | 28 | /** 29 | * Test get with custom schema 30 | * 31 | * @test 32 | */ 33 | public function testGetCustom() 34 | { 35 | $response = $this->call('GET', '/graphql/custom', [ 36 | 'query' => $this->queries['examplesCustom'] 37 | ]); 38 | 39 | $content = $response->getOriginalContent(); 40 | $this->assertArrayHasKey('data', $content); 41 | $this->assertEquals($content['data'], [ 42 | 'examplesCustom' => $this->data 43 | ]); 44 | } 45 | 46 | /** 47 | * Test get with params 48 | * 49 | * @test 50 | */ 51 | public function testGetWithParams() 52 | { 53 | $response = $this->call('GET', '/graphql', [ 54 | 'query' => $this->queries['examplesWithParams'], 55 | 'params' => [ 56 | 'index' => 0 57 | ] 58 | ]); 59 | 60 | $this->assertEquals($response->getStatusCode(), 200); 61 | 62 | $content = $response->getOriginalContent(); 63 | $this->assertArrayHasKey('data', $content); 64 | $this->assertEquals($content['data'], [ 65 | 'examples' => [ 66 | $this->data[0] 67 | ] 68 | ]); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/FieldTest.php: -------------------------------------------------------------------------------- 1 | getFieldClass(); 22 | $field = new $class(); 23 | $attributes = $field->getAttributes(); 24 | 25 | $this->assertArrayHasKey('name', $attributes); 26 | $this->assertArrayHasKey('type', $attributes); 27 | $this->assertArrayHasKey('args', $attributes); 28 | $this->assertArrayHasKey('resolve', $attributes); 29 | $this->assertInternalType('array', $attributes['args']); 30 | $this->assertInstanceOf(Closure::class, $attributes['resolve']); 31 | $this->assertInstanceOf(get_class($field->type()), $attributes['type']); 32 | } 33 | 34 | /** 35 | * Test resolve closure 36 | * 37 | * @test 38 | */ 39 | public function testResolve() 40 | { 41 | $class = $this->getFieldClass(); 42 | $field = $this->getMockBuilder($class) 43 | ->setMethods(['resolve']) 44 | ->getMock(); 45 | 46 | $field->expects($this->once()) 47 | ->method('resolve'); 48 | 49 | $attributes = $field->getAttributes(); 50 | $attributes['resolve'](null, [], [], null); 51 | } 52 | 53 | /** 54 | * Test to array 55 | * 56 | * @test 57 | */ 58 | public function testToArray() 59 | { 60 | $class = $this->getFieldClass(); 61 | $field = new $class(); 62 | $array = $field->toArray(); 63 | 64 | $this->assertInternalType('array', $array); 65 | 66 | $attributes = $field->getAttributes(); 67 | $this->assertEquals($attributes, $array); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/GraphQLQueryTest.php: -------------------------------------------------------------------------------- 1 | queries['examples']); 19 | 20 | $this->assertObjectHasAttribute('data', $result); 21 | 22 | $this->assertEquals($result->data, [ 23 | 'examples' => $this->data 24 | ]); 25 | } 26 | 27 | /** 28 | * Test query methods 29 | * 30 | * @test 31 | */ 32 | public function testQuery() 33 | { 34 | $resultArray = GraphQL::query($this->queries['examples']); 35 | $result = GraphQL::queryAndReturnResult($this->queries['examples']); 36 | 37 | $this->assertInternalType('array', $resultArray); 38 | $this->assertArrayHasKey('data', $resultArray); 39 | $this->assertEquals($resultArray['data'], $result->data); 40 | } 41 | 42 | /** 43 | * Test query with params 44 | * 45 | * @test 46 | */ 47 | public function testQueryAndReturnResultWithParams() 48 | { 49 | $result = GraphQL::queryAndReturnResult($this->queries['examplesWithParams'], [ 50 | 'index' => 0 51 | ]); 52 | 53 | $this->assertObjectHasAttribute('data', $result); 54 | $this->assertCount(0, $result->errors); 55 | $this->assertEquals($result->data, [ 56 | 'examples' => [ 57 | $this->data[0] 58 | ] 59 | ]); 60 | } 61 | 62 | /** 63 | * Test query with initial root 64 | * 65 | * @test 66 | */ 67 | public function testQueryAndReturnResultWithRoot() 68 | { 69 | $result = GraphQL::queryAndReturnResult($this->queries['examplesWithRoot'], null, [ 70 | 'root' => [ 71 | 'test' => 'root' 72 | ] 73 | ]); 74 | 75 | $this->assertObjectHasAttribute('data', $result); 76 | $this->assertCount(0, $result->errors); 77 | $this->assertEquals($result->data, [ 78 | 'examplesRoot' => [ 79 | 'test' => 'root' 80 | ] 81 | ]); 82 | } 83 | 84 | /** 85 | * Test query with context 86 | * 87 | * @test 88 | */ 89 | public function testQueryAndReturnResultWithContext() 90 | { 91 | $result = GraphQL::queryAndReturnResult($this->queries['examplesWithContext'], null, [ 92 | 'context' => [ 93 | 'test' => 'context' 94 | ] 95 | ]); 96 | $this->assertObjectHasAttribute('data', $result); 97 | $this->assertCount(0, $result->errors); 98 | $this->assertEquals($result->data, [ 99 | 'examplesContext' => [ 100 | 'test' => 'context' 101 | ] 102 | ]); 103 | } 104 | 105 | /** 106 | * Test query with schema 107 | * 108 | * @test 109 | */ 110 | public function testQueryAndReturnResultWithSchema() 111 | { 112 | $result = GraphQL::queryAndReturnResult($this->queries['examplesCustom'], null, [ 113 | 'schema' => [ 114 | 'query' => [ 115 | 'examplesCustom' => ExamplesQuery::class 116 | ] 117 | ] 118 | ]); 119 | 120 | $this->assertObjectHasAttribute('data', $result); 121 | $this->assertCount(0, $result->errors); 122 | $this->assertEquals($result->data, [ 123 | 'examplesCustom' => $this->data 124 | ]); 125 | } 126 | 127 | /** 128 | * Test query with error 129 | * 130 | * @test 131 | */ 132 | public function testQueryWithError() 133 | { 134 | $result = GraphQL::query($this->queries['examplesWithError']); 135 | 136 | $this->assertArrayHasKey('data', $result); 137 | $this->assertArrayHasKey('errors', $result); 138 | $this->assertNull($result['data']); 139 | $this->assertCount(1, $result['errors']); 140 | $this->assertArrayHasKey('message', $result['errors'][0]); 141 | $this->assertArrayHasKey('locations', $result['errors'][0]); 142 | } 143 | 144 | /** 145 | * Test query with validation error 146 | * 147 | * @test 148 | */ 149 | public function testQueryWithValidationError() 150 | { 151 | $result = GraphQL::query($this->queries['examplesWithValidation']); 152 | 153 | $this->assertArrayHasKey('data', $result); 154 | $this->assertArrayHasKey('errors', $result); 155 | $this->assertArrayHasKey('validation', $result['errors'][0]); 156 | $this->assertTrue($result['errors'][0]['validation']->has('index')); 157 | } 158 | 159 | /** 160 | * Test query with validation without error 161 | * 162 | * @test 163 | */ 164 | public function testQueryWithValidation() 165 | { 166 | $result = GraphQL::query($this->queries['examplesWithValidation'], [ 167 | 'index' => 0 168 | ]); 169 | 170 | $this->assertArrayHasKey('data', $result); 171 | $this->assertArrayNotHasKey('errors', $result); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /tests/GraphQLTest.php: -------------------------------------------------------------------------------- 1 | assertGraphQLSchema($schema); 23 | $this->assertGraphQLSchemaHasQuery($schema, 'examples'); 24 | $this->assertGraphQLSchemaHasMutation($schema, 'updateExample'); 25 | $this->assertArrayHasKey('Example', $schema->getTypeMap()); 26 | } 27 | 28 | /** 29 | * Test schema with object 30 | * 31 | * @test 32 | */ 33 | public function testSchemaWithSchemaObject() 34 | { 35 | $schemaObject = new Schema([ 36 | 'query' => new ObjectType([ 37 | 'name' => 'Query' 38 | ]), 39 | 'mutation' => new ObjectType([ 40 | 'name' => 'Mutation' 41 | ]), 42 | 'types' => [] 43 | ]); 44 | $schema = GraphQL::schema($schemaObject); 45 | 46 | $this->assertGraphQLSchema($schema); 47 | $this->assertEquals($schemaObject, $schema); 48 | } 49 | 50 | /** 51 | * Test schema with name 52 | * 53 | * @test 54 | */ 55 | public function testSchemaWithName() 56 | { 57 | $schema = GraphQL::schema('custom'); 58 | 59 | $this->assertGraphQLSchema($schema); 60 | $this->assertGraphQLSchemaHasQuery($schema, 'examplesCustom'); 61 | $this->assertGraphQLSchemaHasMutation($schema, 'updateExampleCustom'); 62 | $this->assertArrayHasKey('Example', $schema->getTypeMap()); 63 | } 64 | 65 | /** 66 | * Test schema custom 67 | * 68 | * @test 69 | */ 70 | public function testSchemaWithArray() 71 | { 72 | $schema = GraphQL::schema([ 73 | 'query' => [ 74 | 'examplesCustom' => ExamplesQuery::class 75 | ], 76 | 'mutation' => [ 77 | 'updateExampleCustom' => UpdateExampleMutation::class 78 | ], 79 | 'types' => [ 80 | CustomExampleType::class 81 | ] 82 | ]); 83 | 84 | $this->assertGraphQLSchema($schema); 85 | $this->assertGraphQLSchemaHasQuery($schema, 'examplesCustom'); 86 | $this->assertGraphQLSchemaHasMutation($schema, 'updateExampleCustom'); 87 | $this->assertArrayHasKey('CustomExample', $schema->getTypeMap()); 88 | } 89 | 90 | /** 91 | * Test schema with wrong name 92 | * 93 | * @test 94 | * @expectedException \Folklore\GraphQL\Exception\SchemaNotFound 95 | */ 96 | public function testSchemaWithWrongName() 97 | { 98 | $schema = GraphQL::schema('wrong'); 99 | } 100 | 101 | /** 102 | * Test type 103 | * 104 | * @test 105 | */ 106 | public function testType() 107 | { 108 | $type = GraphQL::type('Example'); 109 | $this->assertInstanceOf(\GraphQL\Type\Definition\ObjectType::class, $type); 110 | 111 | $typeOther = GraphQL::type('Example'); 112 | $this->assertTrue($type === $typeOther); 113 | 114 | $typeOther = GraphQL::type('Example', true); 115 | $this->assertFalse($type === $typeOther); 116 | } 117 | 118 | /** 119 | * Test wrong type 120 | * 121 | * @test 122 | * @expectedException \Folklore\GraphQL\Exception\TypeNotFound 123 | */ 124 | public function testWrongType() 125 | { 126 | $typeWrong = GraphQL::type('ExampleWrong'); 127 | } 128 | 129 | /** 130 | * Test objectType 131 | * 132 | * @test 133 | */ 134 | public function testObjectType() 135 | { 136 | $objectType = new ObjectType([ 137 | 'name' => 'ObjectType' 138 | ]); 139 | $type = GraphQL::objectType($objectType, [ 140 | 'name' => 'ExampleType' 141 | ]); 142 | 143 | $this->assertInstanceOf(\GraphQL\Type\Definition\ObjectType::class, $type); 144 | $this->assertEquals($objectType, $type); 145 | $this->assertEquals($type->name, 'ExampleType'); 146 | } 147 | 148 | public function testObjectTypeFromFields() 149 | { 150 | $type = GraphQL::objectType([ 151 | 'test' => [ 152 | 'type' => Type::string(), 153 | 'description' => 'A test field' 154 | ] 155 | ], [ 156 | 'name' => 'ExampleType' 157 | ]); 158 | 159 | $this->assertInstanceOf(\GraphQL\Type\Definition\ObjectType::class, $type); 160 | $this->assertEquals($type->name, 'ExampleType'); 161 | $fields = $type->getFields(); 162 | $this->assertArrayHasKey('test', $fields); 163 | } 164 | 165 | public function testObjectTypeClass() 166 | { 167 | $type = GraphQL::objectType(ExampleType::class, [ 168 | 'name' => 'ExampleType' 169 | ]); 170 | 171 | $this->assertInstanceOf(\GraphQL\Type\Definition\ObjectType::class, $type); 172 | $this->assertEquals($type->name, 'ExampleType'); 173 | $fields = $type->getFields(); 174 | $this->assertArrayHasKey('test', $fields); 175 | } 176 | 177 | public function testFormatError() 178 | { 179 | $result = GraphQL::queryAndReturnResult($this->queries['examplesWithError']); 180 | $error = GraphQL::formatError($result->errors[0]); 181 | 182 | $this->assertInternalType('array', $error); 183 | $this->assertArrayHasKey('message', $error); 184 | $this->assertArrayHasKey('locations', $error); 185 | $this->assertEquals($error, [ 186 | 'message' => 'Cannot query field "examplesQueryNotFound" on type "Query".', 187 | 'locations' => [ 188 | [ 189 | 'line' => 3, 190 | 'column' => 13 191 | ] 192 | ] 193 | ]); 194 | } 195 | 196 | public function testFormatValidationError() 197 | { 198 | $validator = Validator::make([], [ 199 | 'test' => 'required' 200 | ]); 201 | $validator->fails(); 202 | $validationError = with(new ValidationError('validation'))->setValidator($validator); 203 | $error = new Error('error', null, $validationError); 204 | $error = GraphQL::formatError($error); 205 | 206 | $this->assertInternalType('array', $error); 207 | $this->assertArrayHasKey('validation', $error); 208 | $this->assertTrue($error['validation']->has('test')); 209 | } 210 | 211 | /** 212 | * Test add type 213 | * 214 | * @test 215 | */ 216 | public function testAddType() 217 | { 218 | $this->expectsEvents(TypeAdded::class); 219 | 220 | GraphQL::addType(CustomExampleType::class); 221 | 222 | $types = GraphQL::getTypes(); 223 | $this->assertArrayHasKey('CustomExample', $types); 224 | 225 | $type = app($types['CustomExample']); 226 | $this->assertInstanceOf(CustomExampleType::class, $type); 227 | 228 | $type = GraphQL::type('CustomExample'); 229 | $this->assertInstanceOf(\GraphQL\Type\Definition\ObjectType::class, $type); 230 | } 231 | 232 | /** 233 | * Test add type with a name 234 | * 235 | * @test 236 | */ 237 | public function testAddTypeWithName() 238 | { 239 | GraphQL::addType(ExampleType::class, 'CustomExample'); 240 | 241 | $types = GraphQL::getTypes(); 242 | $this->assertArrayHasKey('CustomExample', $types); 243 | 244 | $type = app($types['CustomExample']); 245 | $this->assertInstanceOf(ExampleType::class, $type); 246 | 247 | $type = GraphQL::type('CustomExample'); 248 | $this->assertInstanceOf(\GraphQL\Type\Definition\ObjectType::class, $type); 249 | } 250 | 251 | /** 252 | * Test get types 253 | * 254 | * @test 255 | */ 256 | public function testGetTypes() 257 | { 258 | $types = GraphQL::getTypes(); 259 | $this->assertArrayHasKey('Example', $types); 260 | 261 | $type = app($types['Example']); 262 | $this->assertInstanceOf(\Folklore\GraphQL\Support\Type::class, $type); 263 | } 264 | 265 | /** 266 | * Test add schema 267 | * 268 | * @test 269 | */ 270 | public function testAddSchema() 271 | { 272 | $this->expectsEvents(SchemaAdded::class); 273 | 274 | GraphQL::addSchema('custom_add', [ 275 | 'query' => [ 276 | 'examplesCustom' => ExamplesQuery::class 277 | ], 278 | 'mutation' => [ 279 | 'updateExampleCustom' => UpdateExampleMutation::class 280 | ], 281 | 'types' => [ 282 | CustomExampleType::class 283 | ] 284 | ]); 285 | 286 | $schemas = GraphQL::getSchemas(); 287 | $this->assertArrayHasKey('custom_add', $schemas); 288 | } 289 | 290 | /** 291 | * Test get schemas 292 | * 293 | * @test 294 | */ 295 | public function testGetSchemas() 296 | { 297 | $schemas = GraphQL::getSchemas(); 298 | $this->assertArrayHasKey('default', $schemas); 299 | $this->assertArrayHasKey('custom', $schemas); 300 | $this->assertInternalType('array', $schemas['default']); 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /tests/GraphiQLTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(app('view')->exists('graphql::graphiql')); 15 | } 16 | 17 | /** 18 | * Test endpoint 19 | * 20 | * @test 21 | */ 22 | public function testEndpoint() 23 | { 24 | $queryPath = route('graphql.query'); 25 | 26 | $response = $this->call('GET', route('graphql.graphiql')); 27 | $this->assertEquals(200, $response->status()); 28 | $this->assertEquals($queryPath, $response->original->graphqlPath); 29 | $content = $response->getContent(); 30 | $this->assertContains($queryPath, $content); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/InterfaceTypeTest.php: -------------------------------------------------------------------------------- 1 | getAttributes(); 18 | 19 | $this->assertArrayHasKey('resolveType', $attributes); 20 | $this->assertInstanceOf(Closure::class, $attributes['resolveType']); 21 | } 22 | 23 | /** 24 | * Test get attributes resolve type 25 | * 26 | * @test 27 | */ 28 | public function testGetAttributesResolveType() 29 | { 30 | $type = $this->getMockBuilder(ExampleInterfaceType::class) 31 | ->setMethods(['resolveType']) 32 | ->getMock(); 33 | 34 | $type->expects($this->once()) 35 | ->method('resolveType'); 36 | 37 | $attributes = $type->getAttributes(); 38 | $attributes['resolveType'](null); 39 | } 40 | 41 | /** 42 | * Test to type 43 | * 44 | * @test 45 | */ 46 | public function testToType() 47 | { 48 | $type = new ExampleInterfaceType(); 49 | $interfaceType = $type->toType(); 50 | 51 | $this->assertInstanceOf(InterfaceType::class, $interfaceType); 52 | 53 | $this->assertEquals($interfaceType->name, $type->name); 54 | 55 | $fields = $interfaceType->getFields(); 56 | $this->assertArrayHasKey('test', $fields); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/MutationTest.php: -------------------------------------------------------------------------------- 1 | set('graphql.types', [ 20 | 'Example' => ExampleType::class 21 | ]); 22 | } 23 | 24 | /** 25 | * Test get rules 26 | * 27 | * @test 28 | */ 29 | public function testGetRules() 30 | { 31 | $class = $this->getFieldClass(); 32 | $field = new $class(); 33 | $rules = $field->getRules(); 34 | 35 | $this->assertInternalType('array', $rules); 36 | $this->assertArrayHasKey('test', $rules); 37 | $this->assertArrayHasKey('test_with_rules', $rules); 38 | $this->assertArrayHasKey('test_with_rules_closure', $rules); 39 | $this->assertEquals($rules['test'], ['required']); 40 | $this->assertEquals($rules['test_with_rules'], ['required']); 41 | $this->assertEquals($rules['test_with_rules_closure'], ['required']); 42 | } 43 | 44 | /** 45 | * Test resolve 46 | * 47 | * @test 48 | */ 49 | public function testResolve() 50 | { 51 | $class = $this->getFieldClass(); 52 | $field = $this->getMockBuilder($class) 53 | ->setMethods(['resolve']) 54 | ->getMock(); 55 | 56 | $field->expects($this->once()) 57 | ->method('resolve'); 58 | 59 | $attributes = $field->getAttributes(); 60 | $attributes['resolve'](null, [ 61 | 'test' => 'test', 62 | 'test_with_rules' => 'test', 63 | 'test_with_rules_closure' => 'test' 64 | ], [], null); 65 | } 66 | 67 | /** 68 | * Test resolve throw validation error 69 | * 70 | * @test 71 | * @expectedException \Folklore\GraphQL\Error\ValidationError 72 | */ 73 | public function testResolveThrowValidationError() 74 | { 75 | $class = $this->getFieldClass(); 76 | $field = new $class(); 77 | 78 | $attributes = $field->getAttributes(); 79 | $attributes['resolve'](null, [], [], null); 80 | } 81 | 82 | /** 83 | * Test validation error 84 | * 85 | * @test 86 | */ 87 | public function testValidationError() 88 | { 89 | $class = $this->getFieldClass(); 90 | $field = new $class(); 91 | 92 | $attributes = $field->getAttributes(); 93 | 94 | try { 95 | $attributes['resolve'](null, [], [], null); 96 | } catch (\Folklore\GraphQL\Error\ValidationError $e) { 97 | $validator = $e->getValidator(); 98 | 99 | $this->assertInstanceOf(Validator::class, $validator); 100 | 101 | $messages = $e->getValidatorMessages(); 102 | $this->assertTrue($messages->has('test')); 103 | $this->assertTrue($messages->has('test_with_rules')); 104 | $this->assertTrue($messages->has('test_with_rules_closure')); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/Objects/CustomExampleType.php: -------------------------------------------------------------------------------- 1 | 'CustomExample', 11 | 'description' => 'An example' 12 | ]; 13 | 14 | public function fields() 15 | { 16 | return [ 17 | 'test' => [ 18 | 'type' => Type::string(), 19 | 'description' => 'A test field' 20 | ] 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/Objects/ErrorFormatter.php: -------------------------------------------------------------------------------- 1 | $e->getMessage() 12 | ]; 13 | 14 | $locations = $e->getLocations(); 15 | if (!empty($locations)) { 16 | $error['locations'] = array_map(function ($loc) { 17 | return $loc->toArray(); 18 | }, $locations); 19 | } 20 | 21 | $previous = $e->getPrevious(); 22 | if ($previous && $previous instanceof ValidationError) { 23 | $error['validation'] = $previous->getValidatorMessages(); 24 | } 25 | 26 | return $error; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Objects/ExampleField.php: -------------------------------------------------------------------------------- 1 | 'example' 10 | ]; 11 | 12 | public function type() 13 | { 14 | return Type::listOf(Type::string()); 15 | } 16 | 17 | public function args() 18 | { 19 | return [ 20 | 'index' => [ 21 | 'name' => 'index', 22 | 'type' => Type::int() 23 | ] 24 | ]; 25 | } 26 | 27 | public function resolve($root, $args) 28 | { 29 | return ['test']; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Objects/ExampleInterfaceType.php: -------------------------------------------------------------------------------- 1 | 'ExampleInterface', 11 | 'description' => 'An example interface' 12 | ]; 13 | 14 | public function resolveType($root) 15 | { 16 | return Type::string(); 17 | } 18 | 19 | public function fields() 20 | { 21 | return [ 22 | 'test' => [ 23 | 'type' => Type::string(), 24 | 'description' => 'A test field' 25 | ] 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Objects/ExampleType.php: -------------------------------------------------------------------------------- 1 | 'Example', 11 | 'description' => 'An example' 12 | ]; 13 | 14 | public function fields() 15 | { 16 | return [ 17 | 'test' => [ 18 | 'type' => Type::string(), 19 | 'description' => 'A test field' 20 | ], 21 | 'test_validation' => ExampleValidationField::class 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Objects/ExampleValidationField.php: -------------------------------------------------------------------------------- 1 | 'example_validation' 13 | ]; 14 | 15 | public function type() 16 | { 17 | return Type::listOf(Type::string()); 18 | } 19 | 20 | public function args() 21 | { 22 | return [ 23 | 'index' => [ 24 | 'name' => 'index', 25 | 'type' => Type::int(), 26 | 'rules' => ['required'] 27 | ] 28 | ]; 29 | } 30 | 31 | public function resolve($root, $args) 32 | { 33 | return ['test']; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Objects/ExamplesContextQuery.php: -------------------------------------------------------------------------------- 1 | 'Examples context query' 11 | ]; 12 | 13 | public function type() 14 | { 15 | return GraphQL::type('Example'); 16 | } 17 | 18 | public function resolve($root, $args, $context) 19 | { 20 | return $context; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Objects/ExamplesQuery.php: -------------------------------------------------------------------------------- 1 | 'examples' 11 | ]; 12 | 13 | public function type() 14 | { 15 | return Type::listOf(GraphQL::type('Example')); 16 | } 17 | 18 | public function args() 19 | { 20 | return [ 21 | 'index' => ['name' => 'index', 'type' => Type::int()] 22 | ]; 23 | } 24 | 25 | public function resolve($root, $args) 26 | { 27 | $data = include(__DIR__.'/data.php'); 28 | 29 | if (isset($args['index'])) { 30 | return [ 31 | $data[$args['index']] 32 | ]; 33 | } 34 | 35 | return $data; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Objects/ExamplesRootQuery.php: -------------------------------------------------------------------------------- 1 | 'Examples root query' 11 | ]; 12 | 13 | public function type() 14 | { 15 | return GraphQL::type('Example'); 16 | } 17 | 18 | public function resolve($root, $args, $context) 19 | { 20 | return $root; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Objects/UpdateExampleMutation.php: -------------------------------------------------------------------------------- 1 | 'updateExample' 11 | ]; 12 | 13 | public function type() 14 | { 15 | return GraphQL::type('Example'); 16 | } 17 | 18 | public function rules() 19 | { 20 | return [ 21 | 'test' => ['required'] 22 | ]; 23 | } 24 | 25 | public function args() 26 | { 27 | return [ 28 | 'test' => [ 29 | 'name' => 'test', 30 | 'type' => Type::string() 31 | ], 32 | 33 | 'test_with_rules' => [ 34 | 'name' => 'test', 35 | 'type' => Type::string(), 36 | 'rules' => ['required'] 37 | ], 38 | 39 | 'test_with_rules_closure' => [ 40 | 'name' => 'test', 41 | 'type' => Type::string(), 42 | 'rules' => function () { 43 | return ['required']; 44 | } 45 | ] 46 | ]; 47 | } 48 | 49 | public function resolve($root, $args) 50 | { 51 | return [ 52 | 'test' => array_get($args, 'test') 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Objects/data.php: -------------------------------------------------------------------------------- 1 | 'Example 1' 6 | ], 7 | [ 8 | 'test' => 'Example 2' 9 | ], 10 | [ 11 | 'test' => 'Example 3' 12 | ] 13 | ]; 14 | -------------------------------------------------------------------------------- /tests/Objects/queries.php: -------------------------------------------------------------------------------- 1 | " 7 | query QueryExamples { 8 | examples { 9 | test 10 | } 11 | } 12 | ", 13 | 14 | 'examplesCustom' => " 15 | query QueryExamplesCustom { 16 | examplesCustom { 17 | test 18 | } 19 | } 20 | ", 21 | 22 | 'examplesWithParams' => " 23 | query QueryExamplesParams(\$index: Int) { 24 | examples(index: \$index) { 25 | test 26 | } 27 | } 28 | ", 29 | 30 | 'examplesWithContext' => " 31 | query QueryExamplesContext { 32 | examplesContext { 33 | test 34 | } 35 | } 36 | ", 37 | 38 | 'examplesWithRoot' => " 39 | query QueryExamplesRoot { 40 | examplesRoot { 41 | test 42 | } 43 | } 44 | ", 45 | 46 | 'examplesWithError' => " 47 | query QueryExamplesWithError { 48 | examplesQueryNotFound { 49 | test 50 | } 51 | } 52 | ", 53 | 54 | 'examplesWithValidation' => " 55 | query QueryExamplesWithValidation(\$index: Int) { 56 | examples { 57 | test_validation(index: \$index) 58 | } 59 | } 60 | ", 61 | 62 | 'updateExampleCustom' => " 63 | mutation UpdateExampleCustom(\$test: String) { 64 | updateExampleCustom(test: \$test) { 65 | test 66 | } 67 | } 68 | " 69 | 70 | ]; 71 | -------------------------------------------------------------------------------- /tests/QueryTest.php: -------------------------------------------------------------------------------- 1 | set('graphql.types', [ 19 | 'Example' => ExampleType::class 20 | ]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | queries = include(__DIR__.'/Objects/queries.php'); 18 | $this->data = include(__DIR__.'/Objects/data.php'); 19 | } 20 | 21 | protected function getEnvironmentSetUp($app) 22 | { 23 | $app['config']->set('graphql.schemas.default', [ 24 | 'query' => [ 25 | 'examples' => ExamplesQuery::class, 26 | 'examplesContext' => ExamplesContextQuery::class, 27 | 'examplesRoot' => ExamplesRootQuery::class 28 | ], 29 | 'mutation' => [ 30 | 'updateExample' => UpdateExampleMutation::class 31 | ] 32 | ]); 33 | 34 | $app['config']->set('graphql.schemas.custom', [ 35 | 'query' => [ 36 | 'examplesCustom' => ExamplesQuery::class 37 | ], 38 | 'mutation' => [ 39 | 'updateExampleCustom' => UpdateExampleMutation::class 40 | ] 41 | ]); 42 | 43 | $app['config']->set('graphql.types', [ 44 | 'Example' => ExampleType::class 45 | ]); 46 | } 47 | 48 | protected function assertGraphQLSchema($schema) 49 | { 50 | $this->assertInstanceOf('GraphQL\Schema', $schema); 51 | } 52 | 53 | protected function assertGraphQLSchemaHasQuery($schema, $key) 54 | { 55 | //Query 56 | $query = $schema->getQueryType(); 57 | $queryFields = $query->getFields(); 58 | $this->assertArrayHasKey($key, $queryFields); 59 | 60 | $queryField = $queryFields[$key]; 61 | $queryListType = $queryField->getType(); 62 | $queryType = $queryListType->getWrappedType(); 63 | $this->assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $queryField); 64 | $this->assertInstanceOf('GraphQL\Type\Definition\ListOfType', $queryListType); 65 | $this->assertInstanceOf('GraphQL\Type\Definition\ObjectType', $queryType); 66 | } 67 | 68 | protected function assertGraphQLSchemaHasMutation($schema, $key) 69 | { 70 | //Mutation 71 | $mutation = $schema->getMutationType(); 72 | $mutationFields = $mutation->getFields(); 73 | $this->assertArrayHasKey($key, $mutationFields); 74 | 75 | $mutationField = $mutationFields[$key]; 76 | $mutationType = $mutationField->getType(); 77 | $this->assertInstanceOf('GraphQL\Type\Definition\FieldDefinition', $mutationField); 78 | $this->assertInstanceOf('GraphQL\Type\Definition\ObjectType', $mutationType); 79 | } 80 | 81 | protected function getPackageProviders($app) 82 | { 83 | return [ 84 | \Folklore\GraphQL\ServiceProvider::class 85 | ]; 86 | } 87 | 88 | protected function getPackageAliases($app) 89 | { 90 | return [ 91 | 'GraphQL' => \Folklore\GraphQL\Support\Facades\GraphQL::class 92 | ]; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/TypeTest.php: -------------------------------------------------------------------------------- 1 | getFields(); 18 | 19 | $this->assertArrayHasKey('test', $fields); 20 | $this->assertEquals($fields['test'], [ 21 | 'type' => Type::string(), 22 | 'description' => 'A test field' 23 | ]); 24 | } 25 | 26 | /** 27 | * Test get attributes 28 | * 29 | * @test 30 | */ 31 | public function testGetAttributes() 32 | { 33 | $type = new ExampleType(); 34 | $attributes = $type->getAttributes(); 35 | 36 | $this->assertArrayHasKey('name', $attributes); 37 | $this->assertArrayHasKey('fields', $attributes); 38 | $this->assertInstanceOf(Closure::class, $attributes['fields']); 39 | $this->assertInternalType('array', $attributes['fields']()); 40 | } 41 | 42 | /** 43 | * Test get attributes fields closure 44 | * 45 | * @test 46 | */ 47 | public function testGetAttributesFields() 48 | { 49 | $type = $this->getMockBuilder(ExampleType::class) 50 | ->setMethods(['getFields']) 51 | ->getMock(); 52 | 53 | $type->expects($this->once()) 54 | ->method('getFields'); 55 | 56 | $attributes = $type->getAttributes(); 57 | $attributes['fields'](); 58 | } 59 | 60 | /** 61 | * Test to array 62 | * 63 | * @test 64 | */ 65 | public function testToArray() 66 | { 67 | $type = new ExampleType(); 68 | $array = $type->toArray(); 69 | 70 | $this->assertInternalType('array', $array); 71 | 72 | $attributes = $type->getAttributes(); 73 | $this->assertEquals($attributes, $array); 74 | } 75 | 76 | /** 77 | * Test to type 78 | * 79 | * @test 80 | */ 81 | public function testToType() 82 | { 83 | $type = new ExampleType(); 84 | $objectType = $type->toType(); 85 | 86 | $this->assertInstanceOf(ObjectType::class, $objectType); 87 | 88 | $this->assertEquals($objectType->name, $type->name); 89 | 90 | $fields = $objectType->getFields(); 91 | $this->assertArrayHasKey('test', $fields); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/fixture/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/fixture/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/laravel", 3 | "description": "The Laravel Framework.", 4 | "keywords": ["framework", "laravel"], 5 | "license": "MIT", 6 | "type": "project", 7 | "require": { 8 | "laravel/framework": "~5.0" 9 | }, 10 | "require-dev": { 11 | "phpunit/phpunit": "~4.0" 12 | }, 13 | "autoload": { 14 | "classmap": [ 15 | "database", 16 | "tests/TestCase.php" 17 | ], 18 | "psr-4": { 19 | "App\\": "app/" 20 | } 21 | }, 22 | "minimum-stability": "dev" 23 | } 24 | -------------------------------------------------------------------------------- /tests/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | --------------------------------------------------------------------------------