├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── api ├── Dockerfile ├── app.php ├── composer.json └── schema.php ├── docker-compose.yml ├── products ├── Dockerfile ├── app.php └── composer.json └── reviews ├── Dockerfile ├── app.php └── composer.json /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | */vendor 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Florian Engelhardt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: api/vendor products/vendor reviews/vendor 2 | 3 | %/vendor: %/composer.lock 4 | docker run --rm -i -u 1000:1000 --tty --volume $$PWD/$*:/app composer install 5 | 6 | %/composer.lock: %/composer.json 7 | docker run --rm -i -u 1000:1000 --tty --volume $$PWD/$*:/app composer update 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL API on top of Microservices 2 | 3 | This is just a port from Node.js/Express to PHP/ReactPHP for Chris Norings article on building a [Serverless GraphQL API on top of a Microservice architecture](https://dev.to/azure/learn-how-you-can-build-a-serverless-graphql-api-on-top-of-a-microservice-architecture-233g). 4 | 5 | ## Getting Started 6 | 7 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. 8 | 9 | ### Prerequisites 10 | 11 | You need to have `docker` and `docker-compose` up and running on your local machine. 12 | 13 | ### Installing 14 | 15 | ```bash 16 | git clone https://github.com/flow-control/php-graphql-microservice.git 17 | cd php-graphql-microservice 18 | make 19 | docker-compose up -d 20 | curl -X POST \ 21 | -H "Content-Type: application/json" \ 22 | --data '{ "query": "{ product (id:1) { id name } }" }' \ 23 | localhost:8000 24 | ``` 25 | 26 | ## Build with 27 | 28 | - [ReactPHP](https://reactphp.org/) 29 | - [graphql-php](https://github.com/webonyx/graphql-php) 30 | - [clue/buzz-react](https://github.com/clue/reactphp-buzz) 31 | 32 | ## License 33 | 34 | MIT, see [LICENSE file](LICENSE). 35 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.1-alpine 2 | WORKDIR /app 3 | ENV PORT=3000 4 | COPY . . 5 | EXPOSE $PORT 6 | ENTRYPOINT ["php", "app.php"] 7 | -------------------------------------------------------------------------------- /api/app.php: -------------------------------------------------------------------------------- 1 | getBody(), true); 27 | $query = $input['query']; 28 | $variableValues = isset($input['variables']) ? $input['variables'] : null; 29 | // just pass query and variables to the GraphQL lib 30 | $promise = GraphQL::promiseToExecute($react, $schema, $query, [], null, $variableValues); 31 | // promiseToExecute will return a ReactPHP Promise, so we can register our then callback 32 | return $promise->then(function(ExecutionResult $result) { 33 | $output = $result->toArray(); 34 | return new Response( 35 | 200, 36 | [ 37 | 'Content-Type' => 'application/json' 38 | ], 39 | json_encode($output, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) 40 | ); 41 | }); 42 | }); 43 | $socket = new \React\Socket\SocketServer('0.0.0.0:' . getenv('PORT')); 44 | $server->listen($socket); 45 | echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; 46 | -------------------------------------------------------------------------------- /api/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "react/http": "^1.5.0", 4 | "webonyx/graphql-php": "^14.11.3" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /api/schema.php: -------------------------------------------------------------------------------- 1 | get('http://product-service:3000/')->then(function($response) { 12 | $rawBody = (string)$response->getBody(); 13 | return json_decode($rawBody); 14 | }); 15 | }; 16 | 17 | $reviewResolver = function ($product) use ($browser) { 18 | return $browser->get('http://review-service:3000/')->then(function($response) { 19 | $rawBody = (string)$response->getBody(); 20 | return json_decode($rawBody); 21 | }); 22 | }; 23 | 24 | // return type definitions 25 | 26 | $productType = new ObjectType([ 27 | 'name' => 'Product', 28 | 'fields' => [ 29 | 'id' => [ 30 | 'type' => Type::id(), 31 | ], 32 | 'name' => [ 33 | 'type' => Type::string(), 34 | ], 35 | 'description' => [ 36 | 'type' => Type::string(), 37 | ] 38 | ] 39 | ]); 40 | 41 | $reviewType = new ObjectType([ 42 | 'name' => 'Review', 43 | 'fields' => [ 44 | 'id' => [ 45 | 'type' => Type::id(), 46 | ], 47 | 'title' => [ 48 | 'type' => Type::string(), 49 | ], 50 | 'grade' => [ 51 | 'type' => Type::int(), 52 | ], 53 | 'comment' => [ 54 | 'type' => Type::string(), 55 | ], 56 | 'product' => [ 57 | 'type' => $productType, 58 | 'resolve' => $productResolver 59 | ] 60 | ] 61 | ]); 62 | 63 | // query defintion 64 | 65 | $query = new ObjectType([ 66 | 'name' => 'Query', 67 | 'fields' => [ 68 | 'echo' => [ 69 | 'type' => Type::string(), 70 | 'args' => [ 71 | 'message' => Type::nonNull(Type::string()), 72 | ], 73 | 'resolve' => function($root, $args) { 74 | return $root['prefix'] . $args['message']; 75 | } 76 | ], 77 | 'product' => [ 78 | 'type' => $productType, 79 | 'args' => [ 80 | 'id' => Type::nonNull(Type::id()), 81 | ], 82 | 'resolve' => $productResolver 83 | ], 84 | 'review' => [ 85 | 'type' => $reviewType, 86 | 'args' => [ 87 | 'id' => Type::nonNull(Type::id()), 88 | ], 89 | 'resolve' => $reviewResolver 90 | ], 91 | 92 | ], 93 | ]); 94 | 95 | // schema packs query and types together 96 | 97 | $schema = new Schema([ 98 | 'query' => $query, 99 | 'types' => [ 100 | $productType 101 | ] 102 | ]); 103 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | 5 | product-service: 6 | build: 7 | context: ./products 8 | ports: 9 | - "8001:3000" 10 | networks: 11 | - microservices 12 | volumes: 13 | - ./products/:/app 14 | 15 | review-service: 16 | build: 17 | context: ./reviews 18 | ports: 19 | - "8002:3000" 20 | networks: 21 | - microservices 22 | volumes: 23 | - ./reviews/:/app 24 | 25 | api: 26 | build: 27 | context: ./api 28 | ports: 29 | - "8000:3000" 30 | networks: 31 | - microservices 32 | volumes: 33 | - ./api/:/app 34 | 35 | networks: 36 | microservices: 37 | -------------------------------------------------------------------------------- /products/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.1-alpine 2 | WORKDIR /app 3 | ENV PORT=3000 4 | COPY . . 5 | EXPOSE $PORT 6 | ENTRYPOINT ["php", "app.php"] 7 | -------------------------------------------------------------------------------- /products/app.php: -------------------------------------------------------------------------------- 1 | 'application/json' 15 | ], 16 | json_encode([ 17 | 'id' => 1, 18 | 'name' => 'Avengers', 19 | 'description' => 'a movie' 20 | ], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) 21 | ); 22 | }); 23 | $socket = new \React\Socket\SocketServer('0.0.0.0:' . getenv('PORT')); 24 | $server->listen($socket); 25 | echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; 26 | -------------------------------------------------------------------------------- /products/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "react/http": "^1.5.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /reviews/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.1-alpine 2 | WORKDIR /app 3 | ENV PORT=3000 4 | COPY . . 5 | EXPOSE $PORT 6 | ENTRYPOINT ["php", "app.php"] 7 | -------------------------------------------------------------------------------- /reviews/app.php: -------------------------------------------------------------------------------- 1 | 'application/json' 15 | ], 16 | json_encode([ 17 | 'id' => 2, 18 | 'title' => 'Oh snap what an ending', 19 | 'grade' => 5, 20 | 'comment' => 'I need therapy after this...', 21 | 'product' => 1 22 | ], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) 23 | ); 24 | }); 25 | $socket = new \React\Socket\SocketServer('0.0.0.0:' . getenv('PORT')); 26 | $server->listen($socket); 27 | echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL; 28 | -------------------------------------------------------------------------------- /reviews/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "react/http": "^1.5.0" 4 | } 5 | } 6 | --------------------------------------------------------------------------------