Skip to content

Commit 7c67bdb

Browse files
authored
Merge pull request #11 from ohmybrew/graph-ql-adjustment
Graph ql adjustment
2 parents 750a3c5 + e87b745 commit 7c67bdb

File tree

6 files changed

+194
-38
lines changed

6 files changed

+194
-38
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# CHANGELOG
22

3+
# 3.0.2
4+
5+
+ Adjusted API to work better with Shopify's implementation of GraphQL (#10)
6+
+ `graph()` call now accepts two arguments, `graph(string $query, array $variables = [])`
7+
38
# Vesion 3.0.1
49

510
+ Fix to obtaining access token

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,34 @@ The return value for the request will be an object containing:
147147
Requests are made using Guzzle.
148148

149149
```php
150-
$api->graph(string $query);
150+
$api->graph(string $query, array $variables = []);
151151
```
152152

153153
+ `query` refers to the full GraphQL query
154+
+ `variables` refers to the variables used for the query (if any)
154155

155156
The return value for the request will be an object containing:
156157

157158
+ `response` the full Guzzle response object
158159
+ `body` the JSON decoded response body
160+
+ `errors` if there was errors or not
161+
162+
Example query:
163+
164+
```php
165+
$result = $api->graph('{ shop { productz(first: 1) { edges { node { handle, id } } } } }');
166+
echo $result->body->shop->products->edges[0]->node->handle; // test-product
167+
```
168+
169+
Example mutation:
170+
171+
```php
172+
$result = $api->graph(
173+
'mutation collectionCreate($input: CollectionInput!) { collectionCreate(input: $input) { userErrors { field message } collection { id } } }',
174+
['input' => ['title' => 'Test Collection']]
175+
);
176+
echo $result->body->collectionCreate->collection->id; // gid://shopify/Collection/63171592234
177+
```
159178

160179
### Checking API limits
161180

src/OhMyBrew/BasicShopifyAPI.php

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -397,14 +397,15 @@ public function getApiCalls(string $type = 'rest', string $key = null)
397397
/**
398398
* Runs a request to the Shopify API.
399399
*
400-
* @param string $query The GraphQL query
400+
* @param string $query The GraphQL query
401+
* @param array $variables The optional variables for the query
401402
*
402403
* @throws \Exception When missing api password is missing for private apps
403404
* @throws \Exception When missing access key is missing for public apps
404405
*
405406
* @return array An array of the Guzzle response, and JSON-decoded body
406407
*/
407-
public function graph(string $query)
408+
public function graph(string $query, array $variables = [])
408409
{
409410
if ($this->shop === null) {
410411
// Shop is requiured
@@ -419,37 +420,45 @@ public function graph(string $query)
419420
throw new Exception('Access token required for public Shopify GraphQL calls');
420421
}
421422

423+
// Build the request
424+
$request = ['query' => $query];
425+
if (count($variables) > 0) {
426+
$request['variables'] = $variables;
427+
}
428+
422429
// Create the request, pass the access token and optional parameters
423430
$response = $this->client->request(
424431
'POST',
425432
"https://{$this->shop}/admin/api/graphql.json",
426433
[
427434
'headers' => [
428435
'X-Shopify-Access-Token' => $this->apiPassword ?? $this->accessToken,
429-
'Content-Type' => 'application/graphql',
436+
'Content-Type' => 'application/json',
430437
],
431-
'body' => $query,
438+
'body' => json_encode($request),
432439
]
433440
);
434441

435442
// Grab the data result and extensions
436443
$body = $this->jsonDecode($response->getBody());
437-
$calls = $body->extensions->cost;
438-
439-
// Update the API call information
440-
$this->apiCallLimits['graph'] = [
441-
'left' => (int) $calls->throttleStatus->currentlyAvailable,
442-
'made' => (int) ($calls->throttleStatus->maximumAvailable - $calls->throttleStatus->currentlyAvailable),
443-
'limit' => (int) $calls->throttleStatus->maximumAvailable,
444-
'restoreRate' => (int) $calls->throttleStatus->restoreRate,
445-
'requestedCost' => (int) $calls->requestedQueryCost,
446-
'actualCost' => (int) $calls->actualQueryCost,
447-
];
444+
if (property_exists($body, 'extensions') && property_exists($body->extensions, 'cost')) {
445+
// Update the API call information
446+
$calls = $body->extensions->cost;
447+
$this->apiCallLimits['graph'] = [
448+
'left' => (int) $calls->throttleStatus->currentlyAvailable,
449+
'made' => (int) ($calls->throttleStatus->maximumAvailable - $calls->throttleStatus->currentlyAvailable),
450+
'limit' => (int) $calls->throttleStatus->maximumAvailable,
451+
'restoreRate' => (int) $calls->throttleStatus->restoreRate,
452+
'requestedCost' => (int) $calls->requestedQueryCost,
453+
'actualCost' => (int) $calls->actualQueryCost,
454+
];
455+
}
448456

449457
// Return Guzzle response and JSON-decoded body
450458
return (object) [
451459
'response' => $response,
452-
'body' => $body->data,
460+
'body' => property_exists($body, 'data') ? $body->data : $body->errors,
461+
'errors' => property_exists($body, 'errors'),
453462
];
454463
}
455464

test/GraphApiTest.php

Lines changed: 103 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,16 @@ class GraphApiTest extends \PHPUnit\Framework\TestCase
1616
*/
1717
public function setUp()
1818
{
19-
$this->query = <<<'QL'
20-
{
21-
shop {
22-
products(first: 2) {
23-
edges {
24-
node {
25-
id
26-
handle
27-
}
28-
}
29-
}
30-
}
31-
}
32-
QL;
19+
// Query call
20+
$this->query = [
21+
'{ shop { products(first: 1) { edges { node { handle id } } } } }',
22+
];
23+
24+
// Mutation call with variables
25+
$this->mutation = [
26+
'mutation collectionCreate($input: CollectionInput!) { collectionCreate(input: $input) { userErrors { field message } collection { id } } }',
27+
['input' => ['title' => 'Test Collection']],
28+
];
3329
}
3430

3531
/**
@@ -52,7 +48,7 @@ public function itShouldReturnBaseUrl()
5248
$api->setShop('example.myshopify.com');
5349
$api->setApiKey('123');
5450
$api->setApiPassword('abc');
55-
$api->graph($this->query);
51+
$api->graph($this->query[0]);
5652

5753
$lastRequest = $mock->getLastRequest()->getUri();
5854
$this->assertEquals('https', $lastRequest->getScheme());
@@ -70,7 +66,7 @@ public function itShouldReturnBaseUrl()
7066
public function itShouldThrowExceptionForMissingDomainOnQuery()
7167
{
7268
$api = new BasicShopifyAPI();
73-
$api->graph($this->query);
69+
$api->graph($this->query[0]);
7470
}
7571

7672
/**
@@ -84,7 +80,7 @@ public function itShouldThrowExceptionForMissingApiPasswordOnPrivateQuery()
8480
{
8581
$api = new BasicShopifyAPI(true);
8682
$api->setShop('example.myshopify.com');
87-
$api->graph($this->query);
83+
$api->graph($this->query[0]);
8884
}
8985

9086
/**
@@ -98,15 +94,15 @@ public function itShouldThrowExceptionForMissingAccessTokenOnPublicQuery()
9894
{
9995
$api = new BasicShopifyAPI();
10096
$api->setShop('example.myshopify.com');
101-
$api->graph($this->query);
97+
$api->graph($this->query[0]);
10298
}
10399

104100
/**
105101
* @test
106102
*
107-
* Should get Guzzle response and JSON body
103+
* Should get Guzzle response and JSON body for success
108104
*/
109-
public function itShouldReturnGuzzleResponseAndJsonBodyWithApiCallLimits()
105+
public function itShouldReturnGuzzleResponseAndJsonBodyForSuccess()
110106
{
111107
$response = new Response(
112108
200,
@@ -123,23 +119,109 @@ public function itShouldReturnGuzzleResponseAndJsonBodyWithApiCallLimits()
123119
$api->setAccessToken('!@#');
124120

125121
// Fake param just to test it receives it
126-
$request = $api->graph($this->query);
122+
$request = $api->graph($this->query[0]);
127123
$data = $mock->getLastRequest()->getUri()->getQuery();
128124
$token_header = $mock->getLastRequest()->getHeader('X-Shopify-Access-Token')[0];
129125

126+
// Assert the response data
130127
$this->assertEquals(true, is_object($request));
131128
$this->assertInstanceOf('GuzzleHttp\Psr7\Response', $request->response);
132129
$this->assertEquals(200, $request->response->getStatusCode());
130+
$this->assertEquals(false, $request->errors);
133131
$this->assertEquals(true, is_object($request->body));
134132
$this->assertEquals('gift-card', $request->body->shop->products->edges[0]->node->handle);
135133
$this->assertEquals('!@#', $token_header);
136134

135+
// Confirm limits have been updated
137136
$this->assertEquals(5, $api->getApiCalls('graph', 'made'));
138137
$this->assertEquals(1000, $api->getApiCalls('graph', 'limit'));
139138
$this->assertEquals(1000 - 5, $api->getApiCalls('graph', 'left'));
140139
$this->assertEquals(['left' => 1000 - 5, 'made' => 5, 'limit' => 1000, 'requestedCost' => 5, 'actualCost' => 5, 'restoreRate' => 50], $api->getApiCalls('graph'));
141140
}
142141

142+
/**
143+
* @test
144+
*
145+
* Should get Guzzle response and JSON body for error
146+
*/
147+
public function itShouldReturnGuzzleResponseForError()
148+
{
149+
$response = new Response(
150+
200,
151+
[],
152+
file_get_contents(__DIR__.'/fixtures/graphql/shop_products_error.json')
153+
);
154+
155+
$mock = new MockHandler([$response]);
156+
$client = new Client(['handler' => $mock]);
157+
158+
$api = new BasicShopifyAPI();
159+
$api->setClient($client);
160+
$api->setShop('example.myshopify.com');
161+
$api->setAccessToken('!@#');
162+
163+
// Fake param just to test it receives it
164+
$request = $api->graph($this->query[0]);
165+
$data = $mock->getLastRequest()->getUri()->getQuery();
166+
$token_header = $mock->getLastRequest()->getHeader('X-Shopify-Access-Token')[0];
167+
168+
// Assert the response
169+
$this->assertEquals(true, is_object($request));
170+
$this->assertInstanceOf('GuzzleHttp\Psr7\Response', $request->response);
171+
$this->assertEquals(200, $request->response->getStatusCode());
172+
$this->assertEquals(true, $request->errors);
173+
$this->assertEquals(true, is_array($request->body));
174+
$this->assertEquals("Field 'productz' doesn't exist on type 'Shop'", $request->body[0]->message);
175+
$this->assertEquals('!@#', $token_header);
176+
177+
// Confirm limits have not been updated since there is no cost
178+
$this->assertEquals(0, $api->getApiCalls('graph', 'made'));
179+
$this->assertEquals(1000, $api->getApiCalls('graph', 'limit'));
180+
$this->assertEquals(0, $api->getApiCalls('graph', 'left'));
181+
$this->assertEquals(['left' => 0, 'made' => 0, 'limit' => 1000, 'restoreRate' => 50, 'requestedCost' => 0, 'actualCost' => 0], $api->getApiCalls('graph'));
182+
}
183+
184+
/**
185+
* @test
186+
*
187+
* Should process query with variables
188+
*/
189+
public function itShouldProcessQueryWithVariables()
190+
{
191+
$response = new Response(
192+
200,
193+
[],
194+
file_get_contents(__DIR__.'/fixtures/graphql/create_collection.json')
195+
);
196+
197+
$mock = new MockHandler([$response]);
198+
$client = new Client(['handler' => $mock]);
199+
200+
$api = new BasicShopifyAPI();
201+
$api->setClient($client);
202+
$api->setShop('example.myshopify.com');
203+
$api->setAccessToken('!@#');
204+
205+
// Fake param just to test it receives it
206+
$request = $api->graph($this->mutation[0], $this->mutation[1]);
207+
$data = $mock->getLastRequest()->getUri()->getQuery();
208+
$token_header = $mock->getLastRequest()->getHeader('X-Shopify-Access-Token')[0];
209+
210+
// Assert the response data
211+
$this->assertEquals(true, is_object($request));
212+
$this->assertInstanceOf('GuzzleHttp\Psr7\Response', $request->response);
213+
$this->assertEquals(200, $request->response->getStatusCode());
214+
$this->assertEquals(true, is_object($request->body));
215+
$this->assertEquals('gid://shopify/Collection/63171592234', $request->body->collectionCreate->collection->id);
216+
$this->assertEquals('!@#', $token_header);
217+
218+
// Confirm limits have been updated
219+
$this->assertEquals(11, $api->getApiCalls('graph', 'made'));
220+
$this->assertEquals(1000, $api->getApiCalls('graph', 'limit'));
221+
$this->assertEquals(1000 - 11, $api->getApiCalls('graph', 'left'));
222+
$this->assertEquals(['left' => 1000 - 11, 'made' => 11, 'limit' => 1000, 'requestedCost' => 11, 'actualCost' => 11, 'restoreRate' => 50], $api->getApiCalls('graph'));
223+
}
224+
143225
/**
144226
* @test
145227
* @expectedException Exception
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"data": {
3+
"collectionCreate": {
4+
"userErrors": [
5+
6+
],
7+
"collection": {
8+
"id": "gid:\/\/shopify\/Collection\/63171592234"
9+
}
10+
}
11+
},
12+
"extensions": {
13+
"cost": {
14+
"requestedQueryCost": 11,
15+
"actualQueryCost": 11,
16+
"throttleStatus": {
17+
"maximumAvailable": 1000,
18+
"currentlyAvailable": 989,
19+
"restoreRate": 50
20+
}
21+
}
22+
}
23+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"errors": [
3+
{
4+
"message": "Field 'productz' doesn't exist on type 'Shop'",
5+
"locations": [
6+
{
7+
"line": 1,
8+
"column": 10
9+
}
10+
],
11+
"fields": [
12+
"query",
13+
"shop",
14+
"productz"
15+
]
16+
}
17+
]
18+
}

0 commit comments

Comments
 (0)