This commit is contained in:
Your Name
2021-07-26 19:46:18 +02:00
parent e7a49138bb
commit aae17f10a6
818 changed files with 70695 additions and 16408 deletions
+5
View File
@@ -0,0 +1,5 @@
preset: laravel
risky: true
enabled:
- declare_strict_types
- unalign_double_arrow
+13
View File
@@ -0,0 +1,13 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
## 1.0.0
### Added
- Add `RequestParser` to convert an incoming HTTP request to one or more `OperationParams`
+16
View File
@@ -0,0 +1,16 @@
The MIT License (MIT)
Copyright (c) 2019 Benedikt Franke
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+39
View File
@@ -0,0 +1,39 @@
# laragraph/utils
[![CI Status](https://github.com/laragraph/utils/workflows/Continuous%20Integration/badge.svg)](https://github.com/laragraph/utils/actions)
[![codecov](https://codecov.io/gh/laragraph/utils/branch/master/graph/badge.svg)](https://codecov.io/gh/laragraph/utils)
[![StyleCI](https://github.styleci.io/repos/228471198/shield?branch=master)](https://github.styleci.io/repos/228471198)
[![Latest Stable Version](https://poser.pugx.org/laragraph/utils/v/stable)](https://packagist.org/packages/laragraph/utils)
[![Total Downloads](https://poser.pugx.org/laragraph/utils/downloads)](https://packagist.org/packages/laragraph/utils)
Utilities for using GraphQL with Laravel
## Installation
Install through composer
```bash
composer require laragraph/utils
```
## Usage
This package holds basic utilities that are useful for building a GraphQL server with Laravel.
If you want to build an application, we recommend using a full framework that integrates the
primitives within this package:
- SDL-first: [Lighthouse](https://github.com/nuwave/lighthouse)
- Code-first: [graphql-laravel](https://github.com/rebing/graphql-laravel)
## Changelog
See [`CHANGELOG.md`](CHANGELOG.md).
## Contributing
See [`CONTRIBUTING.md`](.github/CONTRIBUTING.md).
## License
This package is licensed using the MIT License.
+55
View File
@@ -0,0 +1,55 @@
{
"name": "laragraph/utils",
"type": "library",
"description": "Utilities for using GraphQL with Laravel",
"homepage": "https://github.com/laragraph/utils",
"license": "MIT",
"authors": [
{
"name": "Benedikt Franke",
"email": "benedikt@franke.tech"
}
],
"require": {
"php": "^7.2 || ^8.0",
"illuminate/contracts": "5.6.* || 5.7.* || 5.8.* || ^6 || ^7 || ^8",
"illuminate/http": "5.6.* || 5.7.* || 5.8.* || ^6 || ^7 || ^8",
"thecodingmachine/safe": "^1.1",
"webonyx/graphql-php": "^0.13.2 || ^14"
},
"require-dev": {
"ergebnis/composer-normalize": "^2.11",
"infection/infection": "~0.20",
"jangregor/phpstan-prophecy": "^0.8.1",
"orchestra/testbench": "3.6.* || 3.7.* || 3.8.* || 3.9.* || ^4 || ^5 || ^6",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^0.12.57",
"phpstan/phpstan-deprecation-rules": "^0.12.5",
"phpstan/phpstan-strict-rules": "^0.12.5",
"phpunit/phpunit": "^7.5 || ^8.5",
"thecodingmachine/phpstan-safe-rule": "^1.0"
},
"config": {
"preferred-install": "dist",
"sort-packages": true
},
"autoload": {
"psr-4": {
"Laragraph\\Utils\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Laragraph\\Utils\\Tests\\": "tests/"
},
"files": [
"vendor/symfony/var-dumper/Resources/functions/dump.php"
]
},
"minimum-stability": "dev",
"prefer-stable": true,
"support": {
"issues": "https://github.com/laragraph/utils/issues",
"source": "https://github.com/laragraph/utils"
}
}
+118
View File
@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Laragraph\Utils;
use GraphQL\Server\Helper;
use GraphQL\Server\OperationParams;
use GraphQL\Server\RequestError;
use GraphQL\Utils\Utils;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
class RequestParser
{
/**
* @var \GraphQL\Server\Helper
*/
protected $helper;
public function __construct()
{
$this->helper = new Helper();
}
/**
* Converts an incoming HTTP request to one or more OperationParams.
*
* @return \GraphQL\Server\OperationParams|array<int, \GraphQL\Server\OperationParams>
*
* @throws \GraphQL\Server\RequestError
*/
public function parseRequest(Request $request)
{
$method = $request->getMethod();
$bodyParams = [];
/** @var array<string, mixed> $queryParams */
$queryParams = $request->query();
if ($method === 'POST') {
/**
* Never null, since Symfony defaults to application/x-www-form-urlencoded.
*
* @var string $contentType
*/
$contentType = $request->header('Content-Type');
if (stripos($contentType, 'application/json') !== false) {
/** @var string $content */
$content = $request->getContent();
$bodyParams = \Safe\json_decode($content, true);
if (! is_array($bodyParams)) {
throw new RequestError(
'GraphQL Server expects JSON object or array, but got '.
Utils::printSafeJson($bodyParams)
);
}
} elseif (stripos($contentType, 'application/graphql') !== false) {
/** @var string $content */
$content = $request->getContent();
$bodyParams = ['query' => $content];
} elseif (stripos($contentType, 'application/x-www-form-urlencoded') !== false) {
/** @var array<string, mixed> $bodyParams */
$bodyParams = $request->post();
} elseif (stripos($contentType, 'multipart/form-data') !== false) {
$bodyParams = $this->inlineFiles($request);
} else {
throw new RequestError('Unexpected content type: '.Utils::printSafeJson($contentType));
}
}
return $this->helper->parseRequestParams($method, $bodyParams, $queryParams);
}
/**
* Inline file uploads given through a multipart request.
*
* @param \Illuminate\Http\Request $request
* @return array<mixed>
*/
protected function inlineFiles(Request $request): array
{
/** @var string|null $mapParam */
$mapParam = $request->post('map');
if ($mapParam === null) {
throw new RequestError(
'Could not find a valid map, be sure to conform to GraphQL multipart request specification: https://github.com/jaydenseric/graphql-multipart-request-spec'
);
}
/** @var string|null $operationsParam */
$operationsParam = $request->post('operations');
if ($operationsParam === null) {
throw new RequestError(
'Could not find valid operations, be sure to conform to GraphQL multipart request specification: https://github.com/jaydenseric/graphql-multipart-request-spec'
);
}
/** @var array<string, mixed>|array<int, array<string, mixed>> $operations */
$operations = \Safe\json_decode($operationsParam, true);
/** @var array<int|string, array<int, string>> $map */
$map = \Safe\json_decode($mapParam, true);
foreach ($map as $fileKey => $operationsPaths) {
/** @var array<string> $operationsPaths */
$file = $request->file((string) $fileKey);
/** @var string $operationsPath */
foreach ($operationsPaths as $operationsPath) {
Arr::set($operations, $operationsPath, $file);
}
}
return $operations;
}
}
+253
View File
@@ -0,0 +1,253 @@
<?php
declare(strict_types=1);
namespace Laragraph\Utils\Tests\Unit;
use GraphQL\Server\RequestError;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Laragraph\Utils\RequestParser;
use Orchestra\Testbench\TestCase;
use Safe\Exceptions\JsonException;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
class RequestParserTest extends TestCase
{
public function testGetWithQuery(): void
{
$query = /** @lang GraphQL */ '{ foo }';
$request = $this->makeRequest('GET', ['query' => $query]);
$parser = new RequestParser();
/** @var \GraphQL\Server\OperationParams $params */
$params = $parser->parseRequest($request);
self::assertSame($query, $params->query);
}
public function testPostWithJson(): void
{
$query = /** @lang GraphQL */ '{ foo }';
$request = $this->makeRequest(
'POST',
[],
[],
['Content-Type' => 'application/json'],
\Safe\json_encode(['query' => $query])
);
$parser = new RequestParser();
/** @var \GraphQL\Server\OperationParams $params */
$params = $parser->parseRequest($request);
self::assertSame($query, $params->query);
}
public function testPostWithQueryApplicationGraphQL(): void
{
$query = /** @lang GraphQL */ '{ foo }';
$request = $this->makeRequest(
'POST',
[],
[],
['Content-Type' => 'application/graphql'],
$query
);
$parser = new RequestParser();
/** @var \GraphQL\Server\OperationParams $params */
$params = $parser->parseRequest($request);
self::assertSame($query, $params->query);
}
public function testPostWithRegularForm(): void
{
$query = /** @lang GraphQL */ '{ foo }';
$request = $this->makeRequest(
'POST',
['query' => $query],
[],
['Content-Type' => 'application/x-www-form-urlencoded']
);
$parser = new RequestParser();
/** @var \GraphQL\Server\OperationParams $params */
$params = $parser->parseRequest($request);
self::assertSame($query, $params->query);
}
public function testPostDefaultsToRegularForm(): void
{
$query = /** @lang GraphQL */ '{ foo }';
$request = $this->makeRequest(
'POST',
['query' => $query]
);
$parser = new RequestParser();
/** @var \GraphQL\Server\OperationParams $params */
$params = $parser->parseRequest($request);
self::assertSame($query, $params->query);
}
public function testNonSensicalContentType(): void
{
$request = $this->makeRequest(
'POST',
[],
[],
['Content-Type' => 'foobar']
);
$parser = new RequestParser();
$this->expectException(RequestError::class);
$parser->parseRequest($request);
}
public function testNoQuery(): void
{
$request = $this->makeRequest('GET');
$parser = new RequestParser();
/** @var \GraphQL\Server\OperationParams $params */
$params = $parser->parseRequest($request);
self::assertSame(null, $params->query);
}
public function testInvalidJson(): void
{
$request = $this->makeRequest(
'POST',
[],
[],
['Content-Type' => 'application/json'],
'this is not valid json'
);
$parser = new RequestParser();
$this->expectException(JsonException::class);
$parser->parseRequest($request);
}
public function testNonArrayJson(): void
{
$request = $this->makeRequest(
'POST',
[],
[],
['Content-Type' => 'application/json'],
'"this should be a map with query, variables, etc."'
);
$parser = new RequestParser();
$this->expectException(RequestError::class);
$parser->parseRequest($request);
}
public function testMultipartFormRequest(): void
{
$file = UploadedFile::fake()->create('image.jpg', 500);
$request = $this->makeRequest(
'POST',
[
'operations' => /** @lang JSON */ '
{
"query": "mutation Upload($file: Upload!) { upload(file: $file) }",
"variables": {
"file": null
}
}
',
'map' => /** @lang JSON */ '
{
"0": ["variables.file"]
}
',
],
[
'0' => $file,
],
[
'Content-Type' => 'multipart/form-data',
]
);
$parser = new RequestParser();
/** @var \GraphQL\Server\OperationParams $params */
$params = $parser->parseRequest($request);
self::assertSame('mutation Upload($file: Upload!) { upload(file: $file) }', $params->query);
$variables = $params->variables;
self::assertNotNull($variables);
/** @var array<string, mixed> $variables */
self::assertSame($file, $variables['file']);
}
public function testMultipartFormWithoutMap(): void
{
$request = $this->makeRequest(
'POST',
[],
[],
[
'Content-Type' => 'multipart/form-data',
]
);
$parser = new RequestParser();
$this->expectException(RequestError::class);
$parser->parseRequest($request);
}
public function testMultipartFormWithoutOperations(): void
{
$request = $this->makeRequest(
'POST',
[
'map' => /** @lang JSON */ '
{
"0": ["variables.file"]
}
',
],
[],
[
'Content-Type' => 'multipart/form-data',
]
);
$parser = new RequestParser();
$this->expectException(RequestError::class);
$parser->parseRequest($request);
}
/**
* @param string $method
* @param array<mixed> $parameters
* @param array<mixed> $files
* @param array<mixed> $headers
* @param string|resource|null $content
* @return \Illuminate\Http\Request
*/
public function makeRequest(string $method, array $parameters = [], array $files = [], array $headers = [], $content = null): Request
{
$symfonyRequest = SymfonyRequest::create(
'http://foo.bar/graphql',
$method,
$parameters,
[],
$files,
$this->transformHeadersToServerVars($headers),
$content
);
return Request::createFromBase($symfonyRequest);
}
}