Skip to content

Commit f8c3195

Browse files
committed
Granular methods for HTTP request parsing + tests
1 parent d2cbb0c commit f8c3195

File tree

6 files changed

+556
-99
lines changed

6 files changed

+556
-99
lines changed

src/Server/Helper.php

Lines changed: 136 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class Helper
2828
*
2929
* @return ExecutionResult|Promise
3030
*/
31-
public static function executeOperation(ServerConfig $config, OperationParams $op)
31+
public function executeOperation(ServerConfig $config, OperationParams $op)
3232
{
3333
$phpErrors = [];
3434
$execute = function() use ($config, $op) {
@@ -83,8 +83,12 @@ public static function executeOperation(ServerConfig $config, OperationParams $o
8383
* @param OperationParams $op
8484
* @return string|DocumentNode
8585
*/
86-
private static function loadPersistedQuery(ServerConfig $config, OperationParams $op)
86+
public function loadPersistedQuery(ServerConfig $config, OperationParams $op)
8787
{
88+
if (!$op->queryId) {
89+
throw new InvariantViolation("Could not load persisted query: queryId is not set");
90+
}
91+
8892
// Load query if we got persisted query id:
8993
$loader = $config->getPersistentQueryLoader();
9094

@@ -110,7 +114,7 @@ private static function loadPersistedQuery(ServerConfig $config, OperationParams
110114
* @param OperationParams $params
111115
* @return array
112116
*/
113-
private static function resolveValidationRules(ServerConfig $config, OperationParams $params)
117+
public function resolveValidationRules(ServerConfig $config, OperationParams $params)
114118
{
115119
// Allow customizing validation rules per operation:
116120
$validationRules = $config->getValidationRules();
@@ -129,16 +133,85 @@ private static function resolveValidationRules(ServerConfig $config, OperationPa
129133
return $validationRules;
130134
}
131135

136+
132137
/**
133138
* Parses HTTP request and returns GraphQL QueryParams contained in this request.
134139
* For batched requests it returns an array of QueryParams.
135140
*
136141
* @return OperationParams|OperationParams[]
137142
*/
138-
public static function parseHttpRequest()
143+
public function parseHttpRequest()
144+
{
145+
list ($parsedBody, $isReadonly) = $this->parseRawBody();
146+
return $this->toOperationParams($parsedBody, $isReadonly);
147+
}
148+
149+
/**
150+
* Extracts parsed body and readonly flag from HTTP request
151+
*
152+
* If $readRawBodyFn argument is not provided - will attempt to read raw request body from php://input stream
153+
*
154+
* @param callable|null $readRawBodyFn
155+
* @return array
156+
*/
157+
public function parseRawBody(callable $readRawBodyFn = null)
139158
{
140-
$contentType = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : null;
159+
$method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : null;
141160

161+
if ($method === 'GET') {
162+
$isReadonly = true;
163+
$request = array_change_key_case($_GET);
164+
165+
if (isset($request['query']) || isset($request['queryid']) || isset($request['documentid'])) {
166+
$body = $_GET;
167+
} else {
168+
throw new UserError('Cannot execute GET request without "query" or "queryId" parameter');
169+
}
170+
} else if ($method === 'POST') {
171+
$isReadonly = false;
172+
$contentType = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : null;
173+
174+
if (stripos($contentType, 'application/graphql') !== false) {
175+
$rawBody = $readRawBodyFn ? $readRawBodyFn() : $this->readRawBody();
176+
$body = ['query' => $rawBody ?: ''];
177+
} else if (stripos($contentType, 'application/json') !== false) {
178+
$rawBody = $readRawBodyFn ? $readRawBodyFn() : $this->readRawBody();
179+
$body = json_decode($rawBody ?: '', true);
180+
181+
if (json_last_error()) {
182+
throw new UserError("Could not parse JSON: " . json_last_error_msg());
183+
}
184+
if (!is_array($body)) {
185+
throw new UserError(
186+
"GraphQL Server expects JSON object or array, but got " .
187+
Utils::printSafeJson($body)
188+
);
189+
}
190+
} else if (stripos($contentType, 'application/x-www-form-urlencoded') !== false) {
191+
$body = $_POST;
192+
} else if (null === $contentType) {
193+
throw new UserError('Missing "Content-Type" header');
194+
} else {
195+
throw new UserError("Unexpected content type: " . Utils::printSafeJson($contentType));
196+
}
197+
} else {
198+
throw new UserError('HTTP Method "' . $method . '" is not supported', 405);
199+
}
200+
return [
201+
$body,
202+
$isReadonly
203+
];
204+
}
205+
206+
/**
207+
* Converts parsed body to OperationParams (or list of OperationParams for batched request)
208+
*
209+
* @param $parsedBody
210+
* @param $isReadonly
211+
* @return OperationParams|OperationParams[]
212+
*/
213+
public function toOperationParams($parsedBody, $isReadonly)
214+
{
142215
$assertValid = function (OperationParams $opParams, $queryNum = null) {
143216
$errors = $opParams->validate();
144217
if (!empty($errors[0])) {
@@ -147,45 +220,69 @@ public static function parseHttpRequest()
147220
}
148221
};
149222

150-
if (stripos($contentType, 'application/graphql' !== false)) {
151-
$body = file_get_contents('php://input') ?: '';
152-
$op = OperationParams::create(['query' => $body]);
153-
$assertValid($op);
154-
} else if (stripos($contentType, 'application/json') !== false || stripos($contentType, 'text/json') !== false) {
155-
$body = file_get_contents('php://input') ?: '';
156-
$data = json_decode($body, true);
157-
158-
if (json_last_error()) {
159-
throw new UserError("Could not parse JSON: " . json_last_error_msg());
223+
if (isset($parsedBody[0])) {
224+
// Batched query
225+
$result = [];
226+
foreach ($parsedBody as $index => $entry) {
227+
$op = OperationParams::create($entry, $isReadonly);
228+
$assertValid($op, $index);
229+
$result[] = $op;
160230
}
161-
if (!is_array($data)) {
162-
throw new UserError(
163-
"GraphQL Server expects JSON object or array, but got %s" .
164-
Utils::printSafe($data)
165-
);
166-
}
167-
if (isset($data[0])) {
168-
$op = [];
169-
foreach ($data as $index => $entry) {
170-
$params = OperationParams::create($entry);
171-
$assertValid($params, $index);
172-
$op[] = $params;
231+
} else {
232+
$result = OperationParams::create($parsedBody, $isReadonly);
233+
$assertValid($result);
234+
}
235+
return $result;
236+
}
237+
238+
/**
239+
* @return bool|string
240+
*/
241+
public function readRawBody()
242+
{
243+
return file_get_contents('php://input');
244+
}
245+
246+
/**
247+
* Assertion to check that parsed body is valid instance of OperationParams (or array of instances)
248+
*
249+
* @param $method
250+
* @param $parsedBody
251+
*/
252+
public function assertBodyIsParsedProperly($method, $parsedBody)
253+
{
254+
if (is_array($parsedBody)) {
255+
foreach ($parsedBody as $index => $entry) {
256+
if (!$entry instanceof OperationParams) {
257+
throw new InvariantViolation(sprintf(
258+
'%s expects instance of %s or array of instances. Got invalid array where entry at position %d is %s',
259+
$method,
260+
OperationParams::class,
261+
$index,
262+
Utils::printSafe($entry)
263+
));
264+
}
265+
$errors = $entry->validate();
266+
267+
if (!empty($errors[0])) {
268+
$err = $index ? "Error in query #$index: {$errors[0]}" : $errors[0];
269+
throw new InvariantViolation($err);
173270
}
174-
} else {
175-
$op = OperationParams::create($data);
176-
$assertValid($op);
177271
}
178-
} else if (stripos($contentType, 'application/x-www-form-urlencoded') !== false) {
179-
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
180-
$op = OperationParams::create($_GET, false);
181-
} else {
182-
$op = OperationParams::create($_POST);
272+
}
273+
274+
if ($parsedBody instanceof OperationParams) {
275+
$errors = $parsedBody->validate();
276+
if (!empty($errors[0])) {
277+
throw new InvariantViolation($errors[0]);
183278
}
184-
$assertValid($op);
185-
} else {
186-
throw new UserError("Bad request: unexpected content type: " . Utils::printSafe($contentType));
187279
}
188280

189-
return $op;
281+
throw new InvariantViolation(sprintf(
282+
'%s expects instance of %s or array of instances, but got %s',
283+
$method,
284+
OperationParams::class,
285+
Utils::printSafe($parsedBody)
286+
));
190287
}
191288
}

src/Server/OperationParams.php

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22
namespace GraphQL\Server;
3-
use GraphQL\Utils;
3+
4+
use GraphQL\Utils\Utils;
45

56
/**
67
* Class QueryParams
@@ -82,25 +83,25 @@ public function validate()
8283
$errors[] = 'GraphQL Request must include at least one of those two parameters: "query" or "queryId"';
8384
}
8485
if ($this->query && $this->queryId) {
85-
$errors[] = 'GraphQL Request parameters: "query" and "queryId" are mutually exclusive';
86+
$errors[] = 'GraphQL Request parameters "query" and "queryId" are mutually exclusive';
8687
}
8788

8889
if ($this->query !== null && (!is_string($this->query) || empty($this->query))) {
89-
$errors[] = 'GraphQL Request parameter "query" must be string, but got: ' .
90-
Utils::printSafe($this->query);
90+
$errors[] = 'GraphQL Request parameter "query" must be string, but got ' .
91+
Utils::printSafeJson($this->query);
9192
}
92-
if ($this->queryId !== null && (!is_string($this->query) || empty($this->query))) {
93-
$errors[] = 'GraphQL Request parameter "queryId" must be string, but got: ' .
94-
Utils::printSafe($this->query);
93+
if ($this->queryId !== null && (!is_string($this->queryId) || empty($this->queryId))) {
94+
$errors[] = 'GraphQL Request parameter "queryId" must be string, but got ' .
95+
Utils::printSafeJson($this->queryId);
9596
}
9697

9798
if ($this->operation !== null && (!is_string($this->operation) || empty($this->operation))) {
98-
$errors[] = 'GraphQL Request parameter "operation" must be string, but got: ' .
99-
Utils::printSafe($this->operation);
99+
$errors[] = 'GraphQL Request parameter "operation" must be string, but got ' .
100+
Utils::printSafeJson($this->operation);
100101
}
101102
if ($this->variables !== null && (!is_array($this->variables) || isset($this->variables[0]))) {
102-
$errors[] = 'GraphQL Request parameter "variables" must be associative array, but got: ' .
103-
Utils::printSafe($this->variables);
103+
$errors[] = 'GraphQL Request parameter "variables" must be object, but got ' .
104+
Utils::printSafeJson($this->variables);
104105
}
105106
return $errors;
106107
}

src/Server/StandardServer.php

Lines changed: 9 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,19 @@ public static function create(ServerConfig $config)
3232
*/
3333
private $config;
3434

35+
/**
36+
* @var Helper
37+
*/
38+
private $helper;
39+
3540
/**
3641
* StandardServer constructor.
3742
* @param ServerConfig $config
3843
*/
3944
protected function __construct(ServerConfig $config)
4045
{
4146
$this->config = $config;
47+
$this->helper = new Helper();
4248
}
4349

4450
/**
@@ -48,59 +54,18 @@ protected function __construct(ServerConfig $config)
4854
public function executeRequest($parsedBody = null)
4955
{
5056
if (null !== $parsedBody) {
51-
$this->assertBodyIsParsedProperly(__METHOD__, $parsedBody);
57+
$this->helper->assertBodyIsParsedProperly(__METHOD__, $parsedBody);
5258
} else {
53-
$parsedBody = Helper::parseHttpRequest();
59+
$parsedBody = $this->helper->parseHttpRequest();
5460
}
5561

5662
$batched = is_array($parsedBody);
5763

5864
$result = [];
5965
foreach ((array) $parsedBody as $index => $operationParams) {
60-
$result[] = Helper::executeOperation($this->config, $operationParams);
66+
$result[] = $this->helper->executeOperation($this->config, $operationParams);
6167
}
6268

6369
return $batched ? $result : $result[0];
6470
}
65-
66-
/**
67-
* @param $method
68-
* @param $parsedBody
69-
*/
70-
private function assertBodyIsParsedProperly($method, $parsedBody)
71-
{
72-
if (is_array($parsedBody)) {
73-
foreach ($parsedBody as $index => $entry) {
74-
if (!$entry instanceof OperationParams) {
75-
throw new InvariantViolation(sprintf(
76-
'%s expects instance of %s or array of instances. Got invalid array where entry at position %d is %s',
77-
$method,
78-
OperationParams::class,
79-
$index,
80-
Utils::printSafe($entry)
81-
));
82-
}
83-
$errors = $entry->validate();
84-
85-
if (!empty($errors[0])) {
86-
$err = $index ? "Error in query #$index: {$errors[0]}" : $errors[0];
87-
throw new InvariantViolation($err);
88-
}
89-
}
90-
}
91-
92-
if ($parsedBody instanceof OperationParams) {
93-
$errors = $parsedBody->validate();
94-
if (!empty($errors[0])) {
95-
throw new InvariantViolation($errors[0]);
96-
}
97-
}
98-
99-
throw new InvariantViolation(sprintf(
100-
'%s expects instance of %s or array of instances, but got %s',
101-
$method,
102-
OperationParams::class,
103-
Utils::printSafe($parsedBody)
104-
));
105-
}
10671
}

0 commit comments

Comments
 (0)