├── .php-cs-fixer.php ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── BadMultipartRequestGraphQLException.php ├── BadRequestGraphQLException.php └── RequestParser.php /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | notPath('vendor') 7 | ->in(__DIR__) 8 | ->name('*.php') 9 | ->ignoreDotFiles(true) 10 | ->ignoreVCS(true); 11 | 12 | return risky($finder); 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | [See GitHub releases](https://github.com/laragraph/utils/releases). 9 | 10 | ## Unreleased 11 | 12 | ## v2.2.0 13 | 14 | ### Added 15 | 16 | - Support Laravel 12 https://github.com/laragraph/utils/pull/20 17 | - Support `thecodingmachine/safe` 3 https://github.com/laragraph/utils/pull/21 18 | 19 | ## v2.1.0 20 | 21 | ### Added 22 | 23 | - Support Laravel 11 https://github.com/laragraph/utils/pull/16 24 | 25 | ## v2.0.3 26 | 27 | ### Fixed 28 | 29 | - Do not publish `tests` directory in composer package https://github.com/laragraph/utils/pull/15 30 | 31 | ## v2.0.2 32 | 33 | ### Fixed 34 | 35 | - Throw `BadMultipartRequestGraphQLException` for invalid `multipart/form-data` requests https://github.com/laragraph/utils/pull/14 36 | 37 | ## v2.0.1 38 | 39 | ### Fixed 40 | 41 | - Fix parsing batched queries 42 | 43 | ## v2.0.0 44 | 45 | ### Changed 46 | 47 | - Rely on `$request->input()` instead of manually json decoding request body https://github.com/laragraph/utils/pull/12 48 | 49 | ## v1.6.0 50 | 51 | ### Added 52 | 53 | - Support Laravel 10 54 | 55 | ## v1.5.0 56 | 57 | ### Added 58 | 59 | - Support `webonyx/graphql-php:^15` 60 | 61 | ## v1.4.0 62 | 63 | ### Changed 64 | 65 | - Consistently throw `BadRequestGraphQLException` on invalid requests 66 | 67 | ## v1.3.0 68 | 69 | ### Added 70 | 71 | - Support for Laravel 9 72 | 73 | ## v1.2.0 74 | 75 | ### Added 76 | 77 | - Added support for version `^2` of `thecodingmachine/safe` 78 | 79 | ## v1.1.1 80 | 81 | ### Fixed 82 | 83 | - Fix parsing of complex content types 84 | 85 | ## v1.1.0 86 | 87 | ### Added 88 | 89 | - Recognize `Content-Type: application/graphql+json` in `RequestParser` 90 | 91 | ## v1.0.0 92 | 93 | ### Added 94 | 95 | - Add `RequestParser` to convert an incoming HTTP request to one or more `OperationParams` 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Benedikt Franke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laragraph/utils 2 | 3 | [![CI Status](https://github.com/laragraph/utils/workflows/Continuous%20Integration/badge.svg)](https://github.com/laragraph/utils/actions) 4 | [![codecov](https://codecov.io/gh/laragraph/utils/branch/master/graph/badge.svg)](https://codecov.io/gh/laragraph/utils) 5 | 6 | [![Latest Stable Version](https://poser.pugx.org/laragraph/utils/v/stable)](https://packagist.org/packages/laragraph/utils) 7 | [![Total Downloads](https://poser.pugx.org/laragraph/utils/downloads)](https://packagist.org/packages/laragraph/utils) 8 | 9 | Utilities for using GraphQL with Laravel 10 | 11 | ## Installation 12 | 13 | Install through composer 14 | 15 | ```shell 16 | composer require laragraph/utils 17 | ``` 18 | 19 | ## Usage 20 | 21 | This package holds basic utilities that are useful for building a GraphQL server with Laravel. 22 | If you want to build an application, we recommend using a full framework that integrates the 23 | primitives within this package: 24 | 25 | - SDL-first: [Lighthouse](https://github.com/nuwave/lighthouse) 26 | - Code-first: [graphql-laravel](https://github.com/rebing/graphql-laravel) 27 | 28 | ## Changelog 29 | 30 | See [`CHANGELOG.md`](CHANGELOG.md). 31 | 32 | ## Contributing 33 | 34 | See [`CONTRIBUTING.md`](.github/CONTRIBUTING.md). 35 | 36 | ## License 37 | 38 | This package is licensed using the MIT License. 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laragraph/utils", 3 | "description": "Utilities for using GraphQL with Laravel", 4 | "license": "MIT", 5 | "type": "library", 6 | "authors": [ 7 | { 8 | "name": "Benedikt Franke", 9 | "email": "benedikt@franke.tech" 10 | } 11 | ], 12 | "homepage": "https://github.com/laragraph/utils", 13 | "support": { 14 | "issues": "https://github.com/laragraph/utils/issues", 15 | "source": "https://github.com/laragraph/utils" 16 | }, 17 | "require": { 18 | "php": "^7.2 || ^8", 19 | "illuminate/contracts": "~5.6.0 || ~5.7.0 || ~5.8.0 || ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12", 20 | "illuminate/http": "~5.6.0 || ~5.7.0 || ~5.8.0 || ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12", 21 | "thecodingmachine/safe": "^1.1 || ^2 || ^3", 22 | "webonyx/graphql-php": "^0.13.2 || ^14 || ^15" 23 | }, 24 | "require-dev": { 25 | "ergebnis/composer-normalize": "^2.11", 26 | "jangregor/phpstan-prophecy": "^1", 27 | "mll-lab/php-cs-fixer-config": "^4", 28 | "orchestra/testbench": "~3.6.0 || ~3.7.0 || ~3.8.0 || ~3.9.0 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 || ^10", 29 | "phpstan/extension-installer": "^1", 30 | "phpstan/phpstan": "^1", 31 | "phpstan/phpstan-deprecation-rules": "^1", 32 | "phpstan/phpstan-phpunit": "^1", 33 | "phpstan/phpstan-strict-rules": "^1", 34 | "phpunit/phpunit": "^7.5 || ^8.5 || ^9 || ^10.5 || ^11", 35 | "thecodingmachine/phpstan-safe-rule": "^1.1" 36 | }, 37 | "minimum-stability": "dev", 38 | "prefer-stable": true, 39 | "autoload": { 40 | "psr-4": { 41 | "Laragraph\\Utils\\": "src/" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "Laragraph\\Utils\\Tests\\": "tests/" 47 | }, 48 | "files": [ 49 | "vendor/symfony/var-dumper/Resources/functions/dump.php" 50 | ] 51 | }, 52 | "config": { 53 | "allow-plugins": { 54 | "ergebnis/composer-normalize": true, 55 | "infection/extension-installer": true, 56 | "kylekatarnls/update-helper": true, 57 | "phpstan/extension-installer": true 58 | }, 59 | "preferred-install": "dist", 60 | "sort-packages": true 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/BadMultipartRequestGraphQLException.php: -------------------------------------------------------------------------------- 1 | $headers */ 10 | public function __construct(string $message, ?\Throwable $previous = null, int $code = 0, array $headers = []) 11 | { 12 | parent::__construct( 13 | "{$message} Be sure to conform to the GraphQL multipart request specification (https://github.com/jaydenseric/graphql-multipart-request-spec).", 14 | $previous, 15 | $code, 16 | $headers 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/BadRequestGraphQLException.php: -------------------------------------------------------------------------------- 1 | helper = new Helper(); 22 | } 23 | 24 | /** 25 | * Converts an incoming HTTP request to one or more OperationParams. 26 | * 27 | * @throws \GraphQL\Server\RequestError 28 | * @throws BadRequestGraphQLException 29 | * @throws BadMultipartRequestGraphQLException 30 | * 31 | * @return \GraphQL\Server\OperationParams|array 32 | */ 33 | public function parseRequest(Request $request) 34 | { 35 | $method = $request->getMethod(); 36 | $bodyParams = 'POST' === $method 37 | ? $this->bodyParams($request) 38 | : []; 39 | /** @var array $queryParams Laravel type is not precise enough */ 40 | $queryParams = $request->query(); 41 | 42 | return $this->helper->parseRequestParams($method, $bodyParams, $queryParams); 43 | } 44 | 45 | /** 46 | * Extracts the body parameters from the request. 47 | * 48 | * @throws BadMultipartRequestGraphQLException 49 | * @throws BadRequestGraphQLException 50 | * 51 | * @return array 52 | */ 53 | protected function bodyParams(Request $request): array 54 | { 55 | $contentType = $request->header('Content-Type'); 56 | assert(is_string($contentType), 'Never null, since Symfony defaults to application/x-www-form-urlencoded.'); 57 | 58 | if (Str::startsWith($contentType, 'multipart/form-data')) { 59 | return $this->inlineFiles($request); 60 | } 61 | 62 | if (Str::startsWith($contentType, 'application/graphql') && ! $request->isJson()) { 63 | return ['query' => $request->getContent()]; 64 | } 65 | 66 | $bodyParams = $request->input(); 67 | 68 | if (is_array($bodyParams) && count($bodyParams) > 0) { 69 | if (Arr::isAssoc($bodyParams)) { 70 | return $bodyParams; 71 | } 72 | 73 | $allAssoc = true; 74 | foreach ($bodyParams as $bodyParam) { 75 | if (! is_array($bodyParam) || ! Arr::isAssoc($bodyParam)) { 76 | $allAssoc = false; 77 | } 78 | } 79 | if ($allAssoc) { 80 | return $bodyParams; 81 | } 82 | } 83 | 84 | if ($request->isJson()) { 85 | throw new BadRequestGraphQLException("GraphQL Server expects JSON object or array, but got: {$request->getContent()}."); 86 | } 87 | 88 | throw new BadRequestGraphQLException("Could not decode request with content type: \"{$contentType}\"."); 89 | } 90 | 91 | /** 92 | * Inline file uploads given through a multipart request. 93 | * 94 | * Follows https://github.com/jaydenseric/graphql-multipart-request-spec. 95 | * 96 | * @throws BadMultipartRequestGraphQLException 97 | * 98 | * @return array 99 | */ 100 | protected function inlineFiles(Request $request): array 101 | { 102 | $mapParam = $request->post('map'); 103 | if (null === $mapParam) { 104 | throw new BadMultipartRequestGraphQLException('Missing parameter map.'); 105 | } 106 | if (! is_string($mapParam)) { 107 | $mapParamType = gettype($mapParam); 108 | throw new BadMultipartRequestGraphQLException("Expected parameter map to be a JSON string, got: {$mapParamType}."); 109 | } 110 | 111 | $operationsParam = $request->post('operations'); 112 | if (null === $operationsParam) { 113 | throw new BadMultipartRequestGraphQLException('Missing parameter operations.'); 114 | } 115 | if (! is_string($operationsParam)) { 116 | $operationsParamType = gettype($operationsParam); 117 | throw new BadMultipartRequestGraphQLException("Expected parameter operations to be a JSON string, got: {$operationsParamType}."); 118 | } 119 | 120 | try { 121 | /** Should be array|array>, but it's user input, so it can be anything. */ 122 | $operations = json_decode($operationsParam, true); 123 | } catch (JsonException $e) { 124 | throw new BadMultipartRequestGraphQLException('Parameter operations is not valid JSON.', $e); 125 | } 126 | 127 | if (! is_array($operations)) { 128 | $operationsType = gettype($operations); 129 | throw new BadMultipartRequestGraphQLException("Expected parameter operations to be array, got: {$operationsType}."); 130 | } 131 | 132 | try { 133 | /** Should be array>, but it's user input, so it can be anything */ 134 | $map = json_decode($mapParam, true); 135 | } catch (JsonException $e) { 136 | throw new BadMultipartRequestGraphQLException('Parameter map is not valid JSON.', $e); 137 | } 138 | 139 | if (! is_array($map)) { 140 | $mapType = gettype($map); 141 | throw new BadMultipartRequestGraphQLException("Expected parameter map to be array, got: {$mapType}."); 142 | } 143 | 144 | foreach ($map as $fileKey => $operationsPaths) { 145 | $file = $request->file((string) $fileKey); 146 | 147 | if (! is_iterable($operationsPaths)) { 148 | $operationsPathsType = gettype($operationsPaths); 149 | throw new BadMultipartRequestGraphQLException("Expected map to be array of arrays, got: {$operationsPathsType}."); 150 | } 151 | 152 | foreach ($operationsPaths as $operationsPath) { 153 | if (! is_string($operationsPath)) { 154 | $operationsPathType = gettype($operationsPath); 155 | throw new BadMultipartRequestGraphQLException("Expected map to be array of arrays of strings, got {$operationsPathType}."); 156 | } 157 | Arr::set($operations, $operationsPath, $file); 158 | } 159 | } 160 | 161 | return $operations; 162 | } 163 | } 164 | --------------------------------------------------------------------------------