$value) { if ($name !== '' && $value !== '') { $headerLines[] = $name . ': ' . $value; } } $context = stream_context_create([ 'http' => [ 'method' => $method, 'header' => implode("\r\n", $headerLines), 'content' => $body, 'timeout' => $timeoutSeconds, 'ignore_errors' => true, ], ]); $responseBody = @file_get_contents($url, false, $context); $responseHeaders = $http_response_header ?? []; $status = 0; if (isset($responseHeaders[0]) && preg_match('#HTTP/\S+\s+(\d{3})#', $responseHeaders[0], $matches) === 1) { $status = (int) $matches[1]; } if ($responseBody === false || $responseBody === '') { throw new RuntimeException('Shopify API returned an empty response'); } try { $decoded = json_decode($responseBody, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $exception) { throw new RuntimeException('Shopify API returned invalid JSON', 0, $exception); } if (!is_array($decoded)) { throw new RuntimeException('Shopify API returned a non-object JSON response'); } return ['status' => $status, 'body' => $decoded]; } function shopify_api_credentials(array $env): array { $shop = trim(env_value('SHOPIFY_SHOP', $env)); $clientId = trim(env_value('SHOPIFY_CLIENT_ID', $env)); $clientSecret = trim(env_value('SHOPIFY_CLIENT_SECRET', $env)); $apiVersion = trim(env_value('SHOPIFY_API_VERSION', $env)); if ($shop === '' || $clientId === '' || $clientSecret === '' || $apiVersion === '') { throw new RuntimeException('Incomplete Shopify API configuration'); } if (!preg_match('/^[a-z0-9][a-z0-9-]*\.myshopify\.com$/', $shop)) { throw new RuntimeException('Invalid Shopify shop domain'); } if (!preg_match('/^\d{4}-\d{2}$/', $apiVersion)) { throw new RuntimeException('Invalid Shopify API version'); } return [ 'shop' => $shop, 'client_id' => $clientId, 'client_secret' => $clientSecret, 'api_version' => $apiVersion, ]; } function shopify_api_access_token(array $env): string { $credentials = shopify_api_credentials($env); $body = http_build_query([ 'grant_type' => 'client_credentials', 'client_id' => $credentials['client_id'], 'client_secret' => $credentials['client_secret'], ], '', '&', PHP_QUERY_RFC3986); $response = shopify_api_request_json( 'https://' . $credentials['shop'] . '/admin/oauth/access_token', 'POST', ['Content-Type' => 'application/x-www-form-urlencoded', 'Accept' => 'application/json'], $body ); if ($response['status'] < 200 || $response['status'] >= 300) { throw new RuntimeException('Shopify access-token request failed'); } $token = trim((string) ($response['body']['access_token'] ?? '')); if ($token === '') { throw new RuntimeException('Shopify access-token response did not contain a token'); } return $token; } function shopify_graphql_query(array $env, string $query, array $variables = []): array { if (trim($query) === '') { throw new InvalidArgumentException('Shopify GraphQL query must not be empty'); } $credentials = shopify_api_credentials($env); $token = shopify_api_access_token($env); $body = json_encode(['query' => $query, 'variables' => $variables], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES); $response = shopify_api_request_json( 'https://' . $credentials['shop'] . '/admin/api/' . $credentials['api_version'] . '/graphql.json', 'POST', ['Content-Type' => 'application/json', 'Accept' => 'application/json', 'X-Shopify-Access-Token' => $token], $body ); if ($response['status'] < 200 || $response['status'] >= 300) { throw new RuntimeException('Shopify GraphQL request failed'); } $errors = $response['body']['errors'] ?? []; if (is_array($errors) && $errors !== []) { throw new RuntimeException('Shopify GraphQL returned errors'); } return (array) ($response['body']['data'] ?? []); }