|
1 | | -# GraphQL client for Shopify. |
| 1 | +# GraphQL client for Shopify |
2 | 2 |
|
3 | 3 | [](https://packagist.org/packages/luminarix/laravel-shopify-graphql) |
4 | 4 | [](https://github.com/luminarix/laravel-shopify-graphql/actions?query=workflow%3Arun-tests+branch%3Amain) |
5 | 5 | [](https://github.com/luminarix/laravel-shopify-graphql/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) |
6 | 6 | [](https://packagist.org/packages/luminarix/laravel-shopify-graphql) |
7 | 7 |
|
8 | | -This is a work in progress package to provide a GraphQL client for Shopify. |
| 8 | +A Laravel package for interacting with Shopify's GraphQL Admin API. Built on [Saloon](https://github.com/saloonphp/saloon) with automatic rate limiting and retry handling. |
9 | 9 |
|
10 | 10 | ## Installation |
11 | 11 |
|
12 | | -You can install the package via composer: |
13 | | - |
14 | 12 | ```bash |
15 | 13 | composer require luminarix/laravel-shopify-graphql |
16 | 14 | ``` |
17 | 15 |
|
18 | | -You can publish the config file with: |
| 16 | +Publish the config file: |
19 | 17 |
|
20 | 18 | ```bash |
21 | 19 | php artisan vendor:publish --tag="shopify-graphql-config" |
22 | 20 | ``` |
23 | 21 |
|
24 | | -## Usage |
| 22 | +## Basic Usage |
25 | 23 |
|
26 | 24 | ```php |
27 | 25 | use Luminarix\Shopify\GraphQLClient\Facades\GraphQLClient; |
28 | 26 | use Luminarix\Shopify\GraphQLClient\Authenticators\ShopifyApp; |
29 | 27 |
|
30 | | -$graphql = GraphQLClient::factory(); |
31 | | - |
32 | | -$authenticator = new ShopifyApp($shopDomain, $accessToken, $apiVersion); |
33 | | - |
34 | | -$client = $graphql->create($authenticator) |
| 28 | +$authenticator = new ShopifyApp($shopDomain, $accessToken); |
| 29 | +$client = GraphQLClient::factory()->create($authenticator); |
35 | 30 |
|
36 | 31 | // Query |
37 | | -$query = 'query { |
38 | | - node(id: "gid://shopify/Order/148977776") { |
39 | | - id |
40 | | - ... on Order { |
41 | | - name |
| 32 | +$response = $client->query(' |
| 33 | + query { |
| 34 | + shop { |
| 35 | + name |
| 36 | + email |
| 37 | + } |
42 | 38 | } |
43 | | - } |
44 | | -}'; |
| 39 | +'); |
45 | 40 |
|
46 | | -$response = $client->query($query); |
| 41 | +$data = $response->toArray(); |
47 | 42 |
|
48 | 43 | // Mutation |
49 | | -$mutation = 'mutation orderMarkAsPaid($input: OrderMarkAsPaidInput!) { |
50 | | - orderMarkAsPaid(input: $input) { |
51 | | - order { |
52 | | - # Order fields |
| 44 | +$response = $client->mutate(' |
| 45 | + mutation orderMarkAsPaid($input: OrderMarkAsPaidInput!) { |
| 46 | + orderMarkAsPaid(input: $input) { |
| 47 | + order { |
| 48 | + id |
| 49 | + } |
| 50 | + userErrors { |
| 51 | + field |
| 52 | + message |
| 53 | + } |
| 54 | + } |
53 | 55 | } |
54 | | - userErrors { |
55 | | - field |
56 | | - message |
| 56 | +', [ |
| 57 | + 'input' => [ |
| 58 | + 'id' => 'gid://shopify/Order/123456789', |
| 59 | + ], |
| 60 | +]); |
| 61 | +``` |
| 62 | + |
| 63 | +## Eloquent Model Integration |
| 64 | + |
| 65 | +Create a service class to manage client instances: |
| 66 | + |
| 67 | +```php |
| 68 | +namespace App\Services; |
| 69 | + |
| 70 | +use App\Models\ShopifyShop; |
| 71 | +use Luminarix\Shopify\GraphQLClient\Facades\GraphQLClient; |
| 72 | +use Luminarix\Shopify\GraphQLClient\Authenticators\ShopifyApp; |
| 73 | +use Luminarix\Shopify\GraphQLClient\GraphQLClientMethods; |
| 74 | + |
| 75 | +class ShopifyGraphQLService |
| 76 | +{ |
| 77 | + public function with(ShopifyShop $shop): GraphQLClientMethods |
| 78 | + { |
| 79 | + $authenticator = new ShopifyApp($shop->domain, $shop->access_token); |
| 80 | + |
| 81 | + return GraphQLClient::factory()->create($authenticator); |
57 | 82 | } |
58 | | - } |
59 | | -}'; |
| 83 | +} |
| 84 | +``` |
60 | 85 |
|
61 | | -$variables = [ |
62 | | - 'input' => [ |
63 | | - 'id' => 'gid://shopify/<objectName>/10079785100', |
64 | | - ], |
65 | | -]; |
| 86 | +Create a trait for your models: |
| 87 | + |
| 88 | +```php |
| 89 | +namespace App\Traits; |
| 90 | + |
| 91 | +use App\Services\ShopifyGraphQLService; |
| 92 | +use Luminarix\Shopify\GraphQLClient\GraphQLClientMethods; |
66 | 93 |
|
67 | | -$response = $client->mutation($mutation, $variables); |
| 94 | +trait InteractsWithShopifyGraphQL |
| 95 | +{ |
| 96 | + public function graphql(): GraphQLClientMethods |
| 97 | + { |
| 98 | + return app(ShopifyGraphQLService::class)->with($this); |
| 99 | + } |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +Use on your Shopify shop model: |
| 104 | + |
| 105 | +```php |
| 106 | +namespace App\Models; |
| 107 | + |
| 108 | +use App\Traits\InteractsWithShopifyGraphQL; |
| 109 | +use Illuminate\Database\Eloquent\Model; |
| 110 | + |
| 111 | +class ShopifyShop extends Model |
| 112 | +{ |
| 113 | + use InteractsWithShopifyGraphQL; |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +Now you can call GraphQL directly from your model: |
| 118 | + |
| 119 | +```php |
| 120 | +$shop = ShopifyShop::find(1); |
| 121 | + |
| 122 | +$result = $shop->graphql()->query(' |
| 123 | + query { |
| 124 | + shop { |
| 125 | + name |
| 126 | + } |
| 127 | + } |
| 128 | +')->toArray(); |
| 129 | +``` |
| 130 | + |
| 131 | +## Response Handling |
| 132 | + |
| 133 | +All query and mutation methods return a `GraphQLClientTransformer`: |
| 134 | + |
| 135 | +```php |
| 136 | +$response = $client->query($query); |
| 137 | + |
| 138 | +$response->toArray(); // array |
| 139 | +$response->toCollection(); // Illuminate\Support\Collection |
| 140 | +$response->toFluent(); // Illuminate\Support\Fluent |
| 141 | +$response->toJson(); // string |
| 142 | +$response->toDTO(MyDTO::class); // Custom DTO instance |
| 143 | +``` |
| 144 | + |
| 145 | +The transformer is also equipped with the `Macroable` trait so you can add your own custom methods if needed. |
| 146 | + |
| 147 | +### Getting Response with Extensions |
| 148 | + |
| 149 | +Pass `withExtensions: true` to include cost and rate limit data: |
| 150 | + |
| 151 | +```php |
| 152 | +$response = $client->query($query, withExtensions: true); |
| 153 | +// Returns: ['data' => [...], 'extensions' => ['cost' => [...]]] |
| 154 | +``` |
| 155 | + |
| 156 | +## Bulk Operations |
| 157 | + |
| 158 | +For large data exports, use bulk operations: |
| 159 | + |
| 160 | +```php |
| 161 | +// Start a bulk operation |
| 162 | +$result = $client->createBulkOperation(' |
| 163 | + { |
| 164 | + products { |
| 165 | + edges { |
| 166 | + node { |
| 167 | + id |
| 168 | + title |
| 169 | + } |
| 170 | + } |
| 171 | + } |
| 172 | + } |
| 173 | +'); |
| 174 | + |
| 175 | +// Check current bulk operation status |
| 176 | +$status = $client->getCurrentBulkOperation(); |
| 177 | + |
| 178 | +// Get a specific bulk operation by ID |
| 179 | +$operation = $client->getBulkOperation($numericId); |
| 180 | + |
| 181 | +// Cancel running bulk operation |
| 182 | +$client->cancelBulkOperation(); |
| 183 | +``` |
| 184 | + |
| 185 | +## Rate Limiting |
| 186 | + |
| 187 | +The package automatically handles Shopify's rate limits: |
| 188 | + |
| 189 | +- Tracks query costs from response extensions |
| 190 | +- Waits when throttled before retrying |
| 191 | +- Configurable retry attempts |
| 192 | + |
| 193 | +Get current rate limit info: |
| 194 | + |
| 195 | +```php |
| 196 | +$info = $client->getRateLimitInfo(); |
| 197 | +// [ |
| 198 | +// 'requestedQueryCost' => 10, |
| 199 | +// 'actualQueryCost' => 8, |
| 200 | +// 'maxAvailableLimit' => 1000, |
| 201 | +// 'lastAvailableLimit' => 992, |
| 202 | +// 'restoreRate' => 50, |
| 203 | +// 'isThrottled' => false, |
| 204 | +// ] |
| 205 | +``` |
| 206 | + |
| 207 | +### Custom Rate Limit Service |
| 208 | + |
| 209 | +Implement `RateLimitable` to customize rate limit handling: |
| 210 | + |
| 211 | +```php |
| 212 | +use Luminarix\Shopify\GraphQLClient\Contracts\RateLimitable; |
| 213 | + |
| 214 | +class CustomRateLimitService implements RateLimitable |
| 215 | +{ |
| 216 | + public function getRateLimitInfo(): array { /* ... */ } |
| 217 | + public function updateRateLimitInfo(array $data): void { /* ... */ } |
| 218 | + public function calculateWaitTime(float $requestedQueryCost): float { /* ... */ } |
| 219 | + public function waitIfNecessary(float $requestedQueryCost): void { /* ... */ } |
| 220 | +} |
| 221 | + |
| 222 | +$client = GraphQLClient::factory()->create($authenticator, new CustomRateLimitService()); |
| 223 | +``` |
| 224 | + |
| 225 | +## Configuration |
| 226 | + |
| 227 | +```php |
| 228 | +// config/shopify-graphql.php |
| 229 | +return [ |
| 230 | + // Shopify API version (format: YYYY-MM) |
| 231 | + 'api_version' => env('SHOPIFY_API_VERSION', '2025-01'), |
| 232 | + |
| 233 | + // Fail immediately when throttled (vs. waiting and retrying) |
| 234 | + 'fail_on_throttled' => env('SHOPIFY_FAIL_ON_THROTTLED', true), |
| 235 | + |
| 236 | + // Max retry attempts when throttled |
| 237 | + 'throttle_max_tries' => env('SHOPIFY_THROTTLE_MAX_TRIES', 5), |
| 238 | +]; |
68 | 239 | ``` |
69 | 240 |
|
70 | 241 | ## Testing |
|
0 commit comments