Implementiere read-only Shopify API Client
This commit is contained in:
@@ -0,0 +1,128 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
function shopify_api_request_json(string $url, string $method, array $headers, string $body, int $timeoutSeconds = 20): array
|
||||||
|
{
|
||||||
|
$headerLines = [];
|
||||||
|
foreach ($headers as $name => $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'] ?? []);
|
||||||
|
}
|
||||||
@@ -79,7 +79,7 @@ function shopify_webhook_finish_run(PDO $pdo, int $runId, string $status, array
|
|||||||
function handle_shopify_webhook(PDO $pdo, array $env, string $rawPayload, array $server): array
|
function handle_shopify_webhook(PDO $pdo, array $env, string $rawPayload, array $server): array
|
||||||
{
|
{
|
||||||
$headers = shopify_webhook_headers($server);
|
$headers = shopify_webhook_headers($server);
|
||||||
$secret = env_value('SHOPIFY_API_SECRET', $env);
|
$secret = env_value('SHOPIFY_CLIENT_SECRET', $env);
|
||||||
|
|
||||||
if (($server['REQUEST_METHOD'] ?? '') !== 'POST') {
|
if (($server['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||||||
return ['http_status' => 405, 'payload' => ['status' => 'rejected', 'error' => 'method_not_allowed']];
|
return ['http_status' => 405, 'payload' => ['status' => 'rejected', 'error' => 'method_not_allowed']];
|
||||||
|
|||||||
Reference in New Issue
Block a user