Skip to content

Commit 0e2ac57

Browse files
committed
Split HTTP server execution to canonical replaceable steps: parsing, validation, execution with separate tests for each step
1 parent f8c3195 commit 0e2ac57

File tree

5 files changed

+290
-290
lines changed

5 files changed

+290
-290
lines changed

src/Server/Helper.php

Lines changed: 69 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function executeOperation(ServerConfig $config, OperationParams $op)
3737
if (!$doc instanceof DocumentNode) {
3838
$doc = Parser::parse($doc);
3939
}
40-
if (!$op->allowsMutation() && AST::isMutation($op->operation, $doc)) {
40+
if ($op->isReadOnly() && AST::isMutation($op->operation, $doc)) {
4141
throw new UserError("Cannot execute mutation in read-only context");
4242
}
4343

@@ -133,47 +133,35 @@ public function resolveValidationRules(ServerConfig $config, OperationParams $pa
133133
return $validationRules;
134134
}
135135

136-
137136
/**
138137
* Parses HTTP request and returns GraphQL QueryParams contained in this request.
139138
* For batched requests it returns an array of QueryParams.
140139
*
141-
* @return OperationParams|OperationParams[]
142-
*/
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
140+
* This function doesn't check validity of these params.
151141
*
152142
* If $readRawBodyFn argument is not provided - will attempt to read raw request body from php://input stream
153143
*
154144
* @param callable|null $readRawBodyFn
155-
* @return array
145+
* @return OperationParams|OperationParams[]
156146
*/
157-
public function parseRawBody(callable $readRawBodyFn = null)
147+
public function parseHttpRequest(callable $readRawBodyFn = null)
158148
{
159149
$method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : null;
160150

161151
if ($method === 'GET') {
162-
$isReadonly = true;
163152
$request = array_change_key_case($_GET);
164153

165154
if (isset($request['query']) || isset($request['queryid']) || isset($request['documentid'])) {
166-
$body = $_GET;
155+
$result = OperationParams::create($_GET, true);
167156
} else {
168157
throw new UserError('Cannot execute GET request without "query" or "queryId" parameter');
169158
}
170159
} else if ($method === 'POST') {
171-
$isReadonly = false;
172160
$contentType = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : null;
173161

174162
if (stripos($contentType, 'application/graphql') !== false) {
175163
$rawBody = $readRawBodyFn ? $readRawBodyFn() : $this->readRawBody();
176-
$body = ['query' => $rawBody ?: ''];
164+
$result = OperationParams::create(['query' => $rawBody ?: '']);
177165
} else if (stripos($contentType, 'application/json') !== false) {
178166
$rawBody = $readRawBodyFn ? $readRawBodyFn() : $this->readRawBody();
179167
$body = json_decode($rawBody ?: '', true);
@@ -187,8 +175,17 @@ public function parseRawBody(callable $readRawBodyFn = null)
187175
Utils::printSafeJson($body)
188176
);
189177
}
178+
if (isset($body[0])) {
179+
$result = [];
180+
foreach ($body as $index => $entry) {
181+
$op = OperationParams::create($entry, true);
182+
$result[] = $op;
183+
}
184+
} else {
185+
$result = OperationParams::create($body);
186+
}
190187
} else if (stripos($contentType, 'application/x-www-form-urlencoded') !== false) {
191-
$body = $_POST;
188+
$result = OperationParams::create($_POST);
192189
} else if (null === $contentType) {
193190
throw new UserError('Missing "Content-Type" header');
194191
} else {
@@ -197,92 +194,92 @@ public function parseRawBody(callable $readRawBodyFn = null)
197194
} else {
198195
throw new UserError('HTTP Method "' . $method . '" is not supported', 405);
199196
}
200-
return [
201-
$body,
202-
$isReadonly
203-
];
197+
return $result;
204198
}
205199

206200
/**
207-
* Converts parsed body to OperationParams (or list of OperationParams for batched request)
208-
*
209-
* @param $parsedBody
210-
* @param $isReadonly
211-
* @return OperationParams|OperationParams[]
201+
* @return bool|string
212202
*/
213-
public function toOperationParams($parsedBody, $isReadonly)
203+
public function readRawBody()
214204
{
215-
$assertValid = function (OperationParams $opParams, $queryNum = null) {
216-
$errors = $opParams->validate();
217-
if (!empty($errors[0])) {
218-
$err = $queryNum ? "Error in query #$queryNum: {$errors[0]}" : $errors[0];
219-
throw new UserError($err);
220-
}
221-
};
222-
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;
230-
}
231-
} else {
232-
$result = OperationParams::create($parsedBody, $isReadonly);
233-
$assertValid($result);
234-
}
235-
return $result;
205+
return file_get_contents('php://input');
236206
}
237207

238208
/**
239-
* @return bool|string
209+
* Checks validity of operation params and returns array of errors (empty array when params are valid)
210+
*
211+
* @param OperationParams $params
212+
* @return array
240213
*/
241-
public function readRawBody()
214+
public function validateOperationParams(OperationParams $params)
242215
{
243-
return file_get_contents('php://input');
216+
$errors = [];
217+
if (!$params->query && !$params->queryId) {
218+
$errors[] = 'GraphQL Request must include at least one of those two parameters: "query" or "queryId"';
219+
}
220+
if ($params->query && $params->queryId) {
221+
$errors[] = 'GraphQL Request parameters "query" and "queryId" are mutually exclusive';
222+
}
223+
224+
if ($params->query !== null && (!is_string($params->query) || empty($params->query))) {
225+
$errors[] = 'GraphQL Request parameter "query" must be string, but got ' .
226+
Utils::printSafeJson($params->query);
227+
}
228+
if ($params->queryId !== null && (!is_string($params->queryId) || empty($params->queryId))) {
229+
$errors[] = 'GraphQL Request parameter "queryId" must be string, but got ' .
230+
Utils::printSafeJson($params->queryId);
231+
}
232+
233+
if ($params->operation !== null && (!is_string($params->operation) || empty($params->operation))) {
234+
$errors[] = 'GraphQL Request parameter "operation" must be string, but got ' .
235+
Utils::printSafeJson($params->operation);
236+
}
237+
if ($params->variables !== null && (!is_array($params->variables) || isset($params->variables[0]))) {
238+
$errors[] = 'GraphQL Request parameter "variables" must be object, but got ' .
239+
Utils::printSafeJson($params->variables);
240+
}
241+
return $errors;
244242
}
245243

246244
/**
247245
* Assertion to check that parsed body is valid instance of OperationParams (or array of instances)
248246
*
249-
* @param $method
250-
* @param $parsedBody
247+
* @param OperationParams|OperationParams[] $parsedBody
248+
* @throws InvariantViolation
249+
* @throws UserError
251250
*/
252-
public function assertBodyIsParsedProperly($method, $parsedBody)
251+
public function assertValidRequest($parsedBody)
253252
{
254253
if (is_array($parsedBody)) {
255254
foreach ($parsedBody as $index => $entry) {
256255
if (!$entry instanceof OperationParams) {
257256
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,
257+
'GraphQL Server: Parsed http request must be an instance of %s or array of such instances, '.
258+
'but got invalid array where entry at position %d is %s',
260259
OperationParams::class,
261260
$index,
262261
Utils::printSafe($entry)
263262
));
264263
}
265-
$errors = $entry->validate();
264+
265+
$errors = $this->validateOperationParams($entry);
266266

267267
if (!empty($errors[0])) {
268268
$err = $index ? "Error in query #$index: {$errors[0]}" : $errors[0];
269-
throw new InvariantViolation($err);
269+
throw new UserError($err);
270270
}
271271
}
272-
}
273-
274-
if ($parsedBody instanceof OperationParams) {
275-
$errors = $parsedBody->validate();
272+
} else if ($parsedBody instanceof OperationParams) {
273+
$errors = $this->validateOperationParams($parsedBody);
276274
if (!empty($errors[0])) {
277-
throw new InvariantViolation($errors[0]);
275+
throw new UserError($errors[0]);
278276
}
277+
} else {
278+
throw new InvariantViolation(sprintf(
279+
'GraphQL Server: Parsed http request must be an instance of %s or array of such instances, but got %s',
280+
OperationParams::class,
281+
Utils::printSafe($parsedBody)
282+
));
279283
}
280-
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-
));
287284
}
288285
}

src/Server/OperationParams.php

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,17 @@ class OperationParams
3939
/**
4040
* @var bool
4141
*/
42-
private $allowsMutations;
42+
private $readOnly;
4343

4444
/**
4545
* Creates an instance from given array
4646
*
4747
* @param array $params
48-
* @param bool $allowsMutations
48+
* @param bool $readonly
4949
*
5050
* @return static
5151
*/
52-
public static function create(array $params, $allowsMutations = true)
52+
public static function create(array $params, $readonly = false)
5353
{
5454
$instance = new static();
5555
$instance->originalInput = $params;
@@ -68,44 +68,14 @@ public static function create(array $params, $allowsMutations = true)
6868
$instance->queryId = $params['queryid'] ?: $params['documentid'];
6969
$instance->operation = $params['operation'];
7070
$instance->variables = $params['variables'];
71-
$instance->allowsMutations = (bool) $allowsMutations;
71+
$instance->readOnly = (bool) $readonly;
7272

7373
return $instance;
7474
}
7575

7676
/**
7777
* @return array
7878
*/
79-
public function validate()
80-
{
81-
$errors = [];
82-
if (!$this->query && !$this->queryId) {
83-
$errors[] = 'GraphQL Request must include at least one of those two parameters: "query" or "queryId"';
84-
}
85-
if ($this->query && $this->queryId) {
86-
$errors[] = 'GraphQL Request parameters "query" and "queryId" are mutually exclusive';
87-
}
88-
89-
if ($this->query !== null && (!is_string($this->query) || empty($this->query))) {
90-
$errors[] = 'GraphQL Request parameter "query" must be string, but got ' .
91-
Utils::printSafeJson($this->query);
92-
}
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);
96-
}
97-
98-
if ($this->operation !== null && (!is_string($this->operation) || empty($this->operation))) {
99-
$errors[] = 'GraphQL Request parameter "operation" must be string, but got ' .
100-
Utils::printSafeJson($this->operation);
101-
}
102-
if ($this->variables !== null && (!is_array($this->variables) || isset($this->variables[0]))) {
103-
$errors[] = 'GraphQL Request parameter "variables" must be object, but got ' .
104-
Utils::printSafeJson($this->variables);
105-
}
106-
return $errors;
107-
}
108-
10979
public function getOriginalInput()
11080
{
11181
return $this->originalInput;
@@ -114,8 +84,8 @@ public function getOriginalInput()
11484
/**
11585
* @return bool
11686
*/
117-
public function allowsMutation()
87+
public function isReadOnly()
11888
{
119-
return $this->allowsMutations;
89+
return $this->readOnly;
12090
}
12191
}

src/Server/StandardServer.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,9 @@ protected function __construct(ServerConfig $config)
5454
public function executeRequest($parsedBody = null)
5555
{
5656
if (null !== $parsedBody) {
57-
$this->helper->assertBodyIsParsedProperly(__METHOD__, $parsedBody);
58-
} else {
5957
$parsedBody = $this->helper->parseHttpRequest();
6058
}
59+
$this->helper->assertValidRequest($parsedBody);
6160

6261
$batched = is_array($parsedBody);
6362

0 commit comments

Comments
 (0)