├── .gitignore
├── assets
├── logo.png
└── graphiql.png
├── docker-compose.yml
├── config
└── graphql.php
├── phpunit.xml
├── tests
├── GraphiQLTest.php
├── Queries
│ ├── HeroQuery.php
│ └── HeroQueryPaginated.php
├── Mutations
│ └── HeroMutation.php
├── HeroQueryTest.php
├── HeroMutationTest.php
└── HeroQueryPaginatedTest.php
├── Dockerfile
├── src
├── TypeRegistry.php
├── ServiceProvider.php
├── SchemaFactory.php
├── Query.php
├── Mutation.php
├── GraphQLController.php
└── QueryPaginated.php
├── composer.json
├── README.md
└── resources
└── views
└── graphiql.blade.php
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | composer.lock
3 | .phpcs-cache
4 | .phpunit.result.cache
5 | .idea
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supliu/laravel-graphql/HEAD/assets/logo.png
--------------------------------------------------------------------------------
/assets/graphiql.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/supliu/laravel-graphql/HEAD/assets/graphiql.png
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 |
5 | laravel-graphql:
6 | build: .
7 | ports:
8 | - "8080:80"
9 | volumes:
10 | - .:/app:delegated
11 | - ~/.composer:/home/root/.composer:delegated
--------------------------------------------------------------------------------
/config/graphql.php:
--------------------------------------------------------------------------------
1 | [
14 |
15 | 'default' => [
16 |
17 | 'queries' => [],
18 | 'mutations' => []
19 |
20 | ]
21 | ]
22 | ];
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | ./tests/
11 |
12 |
13 |
14 |
15 |
16 | ./src
17 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/GraphiQLTest.php:
--------------------------------------------------------------------------------
1 | call('GET', '/graphql');
26 |
27 | $crawler->assertViewIs('laravel-graphql::graphiql');
28 | }
29 | }
--------------------------------------------------------------------------------
/tests/Queries/HeroQuery.php:
--------------------------------------------------------------------------------
1 | 'HeroQueryResult',
18 | 'fields' => [
19 | 'name' => Type::string()
20 | ]
21 | ]);
22 | }
23 |
24 | /**
25 | * @return mixed
26 | */
27 | protected function resolve($root, $args, $context, $info)
28 | {
29 | return [
30 | 'name' => 'R2-D2'
31 | ];
32 | }
33 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:7.4-apache
2 |
3 | WORKDIR /app
4 |
5 | RUN apt update
6 |
7 | RUN apt install -y git libzip-dev zip && \
8 | docker-php-ext-install zip
9 |
10 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
11 |
12 | RUN a2enmod rewrite
13 |
14 | RUN rm -rf /var/www/html && \
15 | ln -s /app /var/www/html
16 |
17 | RUN pecl install xdebug && docker-php-ext-enable xdebug
18 |
19 | RUN echo "xdebug.mode=off" | tee -a /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini > /dev/null && \
20 | echo "xdebug.start_with_request=yes" | tee -a /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini > /dev/null && \
21 | echo "xdebug.client_host=host.docker.internal" | tee -a /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini > /dev/null
--------------------------------------------------------------------------------
/src/TypeRegistry.php:
--------------------------------------------------------------------------------
1 | name, self::$types))
29 | self::$types[$type->name] = $type;
30 |
31 | return self::$types[$type->name];
32 | }
33 | }
--------------------------------------------------------------------------------
/tests/Queries/HeroQueryPaginated.php:
--------------------------------------------------------------------------------
1 | 'HeroQueryPaginatedResult',
18 | 'fields' => [
19 | 'name' => Type::string()
20 | ]
21 | ]);
22 | }
23 |
24 | /**
25 | * @return mixed
26 | */
27 | protected function resolve($root, $args, $context, $info)
28 | {
29 | return [
30 | 'total' => 1,
31 | 'current_page' => 1,
32 | 'per_page' => 15,
33 | 'last_page' => 1,
34 | 'data' => [
35 | [
36 | 'name' => 'R2-D2'
37 | ]
38 | ]
39 | ];
40 | }
41 | }
--------------------------------------------------------------------------------
/tests/Mutations/HeroMutation.php:
--------------------------------------------------------------------------------
1 | 'UpdateHeroResult',
15 | 'fields' => [
16 | 'hero_id' => Type::int(),
17 | 'error' => Type::boolean(),
18 | 'message' => Type::string()
19 | ]
20 | ]);
21 | }
22 |
23 | /**
24 | * @return array
25 | */
26 | protected function args(): array
27 | {
28 | return [
29 | 'id' => Type::nonNull(Type::int())
30 | ];
31 | }
32 |
33 | /**
34 | * @return mixed
35 | */
36 | protected function resolve($root, $args, $context, $info)
37 | {
38 | return [
39 | 'hero_id' => $args['id'],
40 | 'error' => false,
41 | 'message' => 'Updated!'
42 | ];
43 | }
44 | }
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "supliu/laravel-graphql",
3 | "description": "GraphQL with Laravel Framework",
4 | "type": "library",
5 | "license": "MIT",
6 | "keywords": [
7 | "laravel", "graphql", "api"
8 | ],
9 | "authors": [
10 | {
11 | "name": "Jansen Felipe",
12 | "email": "jansen.felipe@gmail.com"
13 | }
14 | ],
15 | "autoload": {
16 | "psr-4": {
17 | "Supliu\\LaravelGraphQL\\": "src/"
18 | }
19 | },
20 | "autoload-dev": {
21 | "psr-4": {
22 | "Supliu\\LaravelGraphQL\\Tests\\": "tests/"
23 | }
24 | },
25 | "require": {
26 | "webonyx/graphql-php": "14.*",
27 | "laravel/framework": "^7.30.4|^8.40|^9|~10"
28 | },
29 | "require-dev": {
30 | "orchestra/testbench": "5.*"
31 | },
32 | "extra": {
33 | "branch-alias": {
34 | "dev-master": "1.0-dev"
35 | },
36 | "laravel": {
37 | "providers": [
38 | "Supliu\\LaravelGraphQL\\ServiceProvider"
39 | ]
40 | }
41 | },
42 | "suggest": {
43 | "supliu/laravel-query-monitor": "Simple artisan command to monitoring triggered queries SQL"
44 | },
45 | "minimum-stability": "dev"
46 | }
47 |
--------------------------------------------------------------------------------
/tests/HeroQueryTest.php:
--------------------------------------------------------------------------------
1 | set('graphql.schemas.default.queries', [
30 | 'hero' => \Supliu\LaravelGraphQL\Tests\Queries\HeroQuery::class
31 | ]);
32 | }
33 |
34 | /**
35 | * @test Query Hero
36 | */
37 | public function testQueryHero(): void
38 | {
39 | $query = '
40 | query HeroNameQuery {
41 | hero {
42 | name
43 | }
44 | }
45 | ';
46 |
47 | $crawler = $this->call('POST', '/graphql', ['query' => $query]);
48 |
49 | $crawler->assertJson([
50 | 'data' => [
51 | 'hero' => [
52 | 'name' => 'R2-D2'
53 | ]
54 | ]
55 | ]);
56 | }
57 | }
--------------------------------------------------------------------------------
/tests/HeroMutationTest.php:
--------------------------------------------------------------------------------
1 | set('graphql.schemas.default.mutations', [
30 | 'hero' => \Supliu\LaravelGraphQL\Tests\Mutations\HeroMutation::class
31 | ]);
32 | }
33 |
34 | /**
35 | * @test Query Hero
36 | */
37 | public function testQueryHero(): void
38 | {
39 | $query = '
40 | mutation {
41 | hero (id: 1){
42 | hero_id,
43 | error,
44 | message
45 | }
46 | }
47 | ';
48 |
49 | $crawler = $this->call('POST', '/graphql', ['query' => $query]);
50 |
51 | $crawler->assertJson([
52 | 'data' => [
53 | 'hero' => [
54 | 'hero_id' => '1',
55 | 'error' => false,
56 | 'message' => 'Updated!'
57 | ]
58 | ]
59 | ]);
60 | }
61 | }
--------------------------------------------------------------------------------
/src/ServiceProvider.php:
--------------------------------------------------------------------------------
1 | mergeConfigFrom(
18 | __DIR__.'/../config/graphql.php', 'graphql'
19 | );
20 | }
21 |
22 | /**
23 | * Bootstrap any application services.
24 | *
25 | * @return void
26 | */
27 | public function boot()
28 | {
29 | /*
30 | * Load views
31 | */
32 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'laravel-graphql');
33 |
34 | /*
35 | * Publish config
36 | */
37 | $this->publishes([
38 | __DIR__.'/../config/graphql.php' => config_path('graphql.php')
39 | ], 'config');
40 |
41 | /*
42 | * Publish view
43 | */
44 | $this->publishes([
45 | __DIR__.'/../resources/views' => base_path('resources/views/vendor/laravel-graphql'),
46 | ], 'views');
47 |
48 | /*
49 | * Add routers
50 | */
51 | $router = $this->app['router'];
52 |
53 | $router->get('/graphql', 'Supliu\LaravelGraphQL\GraphQLController@index');
54 | $router->post('/graphql', 'Supliu\LaravelGraphQL\GraphQLController@executeQuery');
55 | }
56 |
57 | protected function bootPublishes()
58 | {
59 |
60 | }
61 | }
--------------------------------------------------------------------------------
/src/SchemaFactory.php:
--------------------------------------------------------------------------------
1 | $this->createQuery($ref['queries']),
29 | 'mutation' => $this->createMutation($ref['mutations'])
30 | ]);
31 | }
32 |
33 | /**
34 | * @param array $queries
35 | * @return ObjectType
36 | */
37 | private function createQuery(array $queries)
38 | {
39 | $fields = [];
40 |
41 | foreach ($queries as $key => $value)
42 | $fields[$key] = app()->make($value)->getConfig();
43 |
44 | return new ObjectType([
45 | 'name' => 'QueryRoot',
46 | 'fields' => $fields
47 | ]);;
48 | }
49 |
50 | /**
51 | * @param array $mutations
52 | * @return ObjectType
53 | */
54 | private function createMutation(array $mutations)
55 | {
56 | $fields = [];
57 |
58 | foreach ($mutations as $key => $value)
59 | $fields[$key] = app()->make($value)->getConfig();
60 |
61 | return new ObjectType([
62 | 'name' => 'MutationRoot',
63 | 'fields' => $fields
64 | ]);
65 | }
66 | }
--------------------------------------------------------------------------------
/tests/HeroQueryPaginatedTest.php:
--------------------------------------------------------------------------------
1 | set('graphql.schemas.default.queries', [
30 | 'heros' => \Supliu\LaravelGraphQL\Tests\Queries\HeroQueryPaginated::class
31 | ]);
32 | }
33 |
34 | /**
35 | * @test Query Hero
36 | */
37 | public function testQueryHero(): void
38 | {
39 | $query = '
40 | query HeroNameQuery {
41 | heros {
42 | total
43 | current_page,
44 | per_page,
45 | last_page,
46 | data {
47 | name
48 | }
49 | }
50 | }
51 | ';
52 |
53 | $crawler = $this->call('POST', '/graphql', ['query' => $query]);
54 |
55 | $crawler->assertJson( [
56 | 'data' => [
57 | 'heros' => [
58 | 'total' => 1,
59 | 'current_page' => 1,
60 | 'per_page' => 15,
61 | 'last_page' => 1,
62 | 'data' => [
63 | [
64 | 'name' => 'R2-D2'
65 | ]
66 | ]
67 | ]
68 | ]
69 | ]);
70 | }
71 | }
--------------------------------------------------------------------------------
/src/Query.php:
--------------------------------------------------------------------------------
1 | config = [
28 | 'type' => $this->typeResult(),
29 | 'args' => $this->resolveArgs(),
30 | 'resolve' => function ($root, $args, $context, $info) {
31 |
32 | if(!$this->authorize())
33 | throw new UnauthorizedException("Unauthorized.");
34 |
35 | if(!empty($this->rules()))
36 | Validator::make($args, $this->rules())->validate();
37 |
38 | return $this->resolve($root, $args, $context, $info);
39 | }
40 | ];
41 | }
42 |
43 | /**
44 | * @return mixed
45 | */
46 | protected abstract function resolve($root, $args, $context, $info);
47 |
48 | /**
49 | * @return boolean
50 | */
51 | protected function authorize(): bool
52 | {
53 | return true;
54 | }
55 |
56 | /**
57 | * @return array
58 | */
59 | protected function rules(): array
60 | {
61 | return [];
62 | }
63 |
64 | /**
65 | * @return array
66 | */
67 | protected function args(): array
68 | {
69 | return [];
70 | }
71 |
72 | /**
73 | * @return Type
74 | */
75 | protected function typeResult(): Type
76 | {
77 | return Type::boolean();
78 | }
79 |
80 | /**
81 | * @return array
82 | */
83 | private function resolveArgs(): array
84 | {
85 | $transformed = collect($this->args())->transform(function ($item, $key){
86 |
87 | if(is_array($item))
88 | return $item;
89 |
90 | return [
91 | 'type' => $item,
92 | 'description' => $key
93 | ];
94 | });
95 |
96 | return $transformed->toArray();
97 | }
98 |
99 | /**
100 | * @return array
101 | */
102 | public function getConfig(): array
103 | {
104 | return $this->config;
105 | }
106 | }
--------------------------------------------------------------------------------
/src/Mutation.php:
--------------------------------------------------------------------------------
1 | config = [
22 | 'type' => $this->typeResult(),
23 | 'args' => $this->resolveArgs(),
24 | 'resolve' => $this->resolveFunction()
25 | ];
26 | }
27 |
28 | /**
29 | * @return mixed
30 | */
31 | protected abstract function resolve($root, $args, $context, $info);
32 |
33 | /**
34 | * @return boolean
35 | */
36 | protected function authorize(): bool
37 | {
38 | return true;
39 | }
40 |
41 | /**
42 | * @return array
43 | */
44 | protected function rules(): array
45 | {
46 | return [];
47 | }
48 |
49 | /**
50 | * @return array
51 | */
52 | protected function args(): array
53 | {
54 | return [];
55 | }
56 |
57 | /**
58 | * @return Type
59 | */
60 | protected function typeResult(): Type
61 | {
62 | return Type::boolean();
63 | }
64 |
65 | /**
66 | * @return \Closure
67 | */
68 | private function resolveFunction()
69 | {
70 | return function ($root, $args, $context, $info) {
71 |
72 | if(!$this->authorize())
73 | throw new UnauthorizedException("Unauthorized.");
74 |
75 | if(!empty($this->rules()))
76 | Validator::make($args, $this->rules())->validate();
77 |
78 | return $this->resolve($root, $args, $context, $info);
79 | };
80 | }
81 |
82 | /**
83 | * @return array
84 | */
85 | private function resolveArgs(): array
86 | {
87 | $transformed = collect($this->args())->transform(function ($item, $key){
88 |
89 | if(is_array($item))
90 | return $item;
91 |
92 | return [
93 | 'type' => $item,
94 | 'description' => $key
95 | ];
96 | });
97 |
98 | return $transformed->toArray();
99 | }
100 |
101 | /**
102 | * @return array
103 | */
104 | public function getConfig(): array
105 | {
106 | return $this->config;
107 | }
108 | }
--------------------------------------------------------------------------------
/src/GraphQLController.php:
--------------------------------------------------------------------------------
1 | get('schema', 'default');
33 |
34 | /*
35 | * Get query
36 | */
37 | $query = $request->get('query');
38 |
39 | /*
40 | * Get variables
41 | */
42 | $variables = $request->get('variables');
43 |
44 | /*
45 | * Check operations
46 | */
47 | if($request->has('operations')) {
48 |
49 | $operations = json_decode($request->get('operations'), true);
50 |
51 | $query = $operations['query'];
52 | $variables = $operations['variables'];
53 | }
54 |
55 | /*
56 | * Execute Query
57 | */
58 | $result = $graphQL->executeQuery($schemaFactory->create($schema), $query, null, null, $variables);
59 |
60 | /*
61 | * Show error
62 | */
63 |
64 | if(count($result->errors) > 0){
65 |
66 | $errors = [];
67 |
68 | /*
69 | * Check previous and report
70 | */
71 | foreach ($result->errors as $error){
72 |
73 | $previous = $error->getPrevious();
74 |
75 | if(!is_null($previous)){
76 |
77 | report($previous);
78 |
79 | if($previous instanceof ValidationException)
80 | foreach ($previous->validator->errors()->all() as $row)
81 | $errors[] = ['message' => $row];
82 | }
83 |
84 | $errors[] = ['message' => $error->getMessage()];
85 | }
86 |
87 | return response()->json(['errors' => $errors]);
88 | }
89 |
90 | /*
91 | * To json
92 | */
93 | return response()->json($result->toArray());
94 | }
95 | }
--------------------------------------------------------------------------------
/src/QueryPaginated.php:
--------------------------------------------------------------------------------
1 | config = [
35 | 'type' => new ObjectType([
36 | 'name' => $className . 'List',
37 | 'fields' => [
38 | 'total' => Type::int(),
39 | 'current_page' => Type::int(),
40 | 'per_page' => Type::int(),
41 | 'last_page' => Type::int(),
42 | 'data' => Type::listOf($this->typeResult())
43 | ]
44 | ]),
45 | 'args' => $this->resolveArgs(),
46 | 'resolve' => $this->resolveFunction()
47 | ];
48 | }
49 |
50 | /**
51 | * @return mixed
52 | */
53 | protected abstract function resolve($root, $args, $context, $info);
54 |
55 | /**
56 | * @return boolean
57 | */
58 | protected function authorize(): bool
59 | {
60 | return true;
61 | }
62 |
63 | /**
64 | * @return array
65 | */
66 | protected function rules(): array
67 | {
68 | return [];
69 | }
70 |
71 | /**
72 | * @return array
73 | */
74 | protected function args(): array
75 | {
76 | return [];
77 | }
78 |
79 | /**
80 | * @return Type
81 | */
82 | protected function typeResult(): Type
83 | {
84 | return Type::boolean();
85 | }
86 |
87 | /**
88 | * @return \Closure
89 | */
90 | private function resolveFunction()
91 | {
92 | return function ($root, $args, $context, $info) {
93 |
94 | if(!$this->authorize())
95 | throw new UnauthorizedException("Unauthorized.");
96 |
97 | if(!empty($this->rules()))
98 | Validator::make($args, $this->rules())->validate();
99 |
100 | $this->resolveInfo = $info;
101 |
102 | Paginator::currentPageResolver(function ($pageName = 'page') use ($args) {
103 |
104 | return isset($args[$pageName]) ? $args[$pageName] : 1;
105 |
106 | });
107 |
108 | return $this->resolve($root, $args, $context, $info);
109 | };
110 | }
111 |
112 | /**
113 | * @return array
114 | */
115 | private function resolveArgs(): array
116 | {
117 | $args = array_merge($this->args(), [
118 | 'page' => Type::int(),
119 | 'limit' => Type::int()
120 | ]);
121 |
122 | $transformed = collect($args)->transform(function ($item, $key){
123 |
124 | if(is_array($item))
125 | return $item;
126 |
127 | return [
128 | 'type' => $item,
129 | 'description' => $key
130 | ];
131 | });
132 |
133 | return $transformed->toArray();
134 | }
135 |
136 | /**
137 | * @return array
138 | */
139 | public function getConfig(): array
140 | {
141 | return $this->config;
142 | }
143 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Laravel GraphQL
6 |
7 | [](//packagist.org/packages/supliu/laravel-graphql) [](//packagist.org/packages/supliu/laravel-graphql) [](//packagist.org/packages/supliu/laravel-graphql) [](//packagist.org/packages/phpunit/phpunit)
8 |
9 | The objective of this project is to facilitate the integration of the webonyx/graphql-php with the Laravel Framework
10 |
11 |
12 |
13 |
14 |
15 | ## How to install
16 |
17 | Use composer to install this package
18 |
19 | ```ssh
20 | composer require supliu/laravel-graphql
21 | ```
22 |
23 | Execute a publish with artisan command:
24 |
25 | ```
26 | php artisan vendor:publish --provider="Supliu\LaravelGraphQL\ServiceProvider"
27 | ```
28 |
29 | ## How to use
30 |
31 | You must create your Query and Mutation classes and register on `config/graphql.php` so that GraphQL can read.
32 |
33 | ```php
34 | 'queries' => [
35 | 'detailHero' => \App\GraphQL\Queries\DetailHero::class
36 | ],
37 |
38 | 'mutations' => [
39 | 'updateHero' => \App\GraphQL\Mutations\UpdateHero::class
40 | ]
41 | ```
42 |
43 | ### Query
44 |
45 | Below is an example of a Query class that returns the data of a Star Wars hero:
46 |
47 | ```php
48 | Type::nonNull(Type::int())
65 | ];
66 | }
67 |
68 | /**
69 | * @return Type
70 | */
71 | protected function typeResult(): Type
72 | {
73 | return new ObjectType([
74 | 'name' => 'HeroQueryResult',
75 | 'fields' => [
76 | 'name' => Type::string()
77 | ]
78 | ]);
79 | }
80 |
81 | /**
82 | * @return mixed
83 | */
84 | protected function resolve($root, $args, $context, $info)
85 | {
86 | return Hero::find($args['id']);
87 | }
88 | }
89 | ```
90 |
91 | ### Mutation
92 |
93 | Below is an example of a Mutation class that returns if update worked:
94 |
95 | ```php
96 | 'UpdateHeroResult',
110 | 'fields' => [
111 | 'error' => Type::boolean(),
112 | 'message' => Type::string()
113 | ]
114 | ]);
115 | }
116 |
117 | /**
118 | * @return array
119 | */
120 | protected function args(): array
121 | {
122 | return [
123 | 'id' => Type::nonNull(Type::int())
124 | 'name' => Type::nonNull(Type::string())
125 | ];
126 | }
127 |
128 | /**
129 | * @return mixed
130 | */
131 | protected function resolve($root, $args, $context, $info)
132 | {
133 | Hero::find($args['id'])->update([
134 | 'name' => $args['name']
135 | ]);
136 |
137 | return [
138 | 'error' => false,
139 | 'message' => 'Updated!'
140 | ];
141 | }
142 | }
143 | ```
144 |
145 | ## License
146 |
147 | The Laravel GraphQL is open-sourced project licensed under the [MIT license](https://opensource.org/licenses/MIT).
148 |
--------------------------------------------------------------------------------
/resources/views/graphiql.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
44 |
51 |
52 |
53 |
54 |
55 |
56 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
81 |
82 |
83 |
84 | Loading...
85 |
232 |
233 |