Iâm not saying that the GraphQL isnât without any value at all. When you make the right calls, it is fast and does return more accurate error messages. But truth be told the main reason developers are using it, is because of of the features which are not available on the REST api. Itâs a sneaky way of pushing devs to use the GraphQl Api. If both apis had perfectly equal features, Graphql usage would be very low.
If I had an app that only made a few different calls to the api, I suppose it would be fine to create a few explicit graph queries and reuse those.
However SEO King makes calls across 10-20 different endpoints with different permutations of queries and variables.
Below is half of my GraphQl class i had to write - i canât fit the entire class within the 32000 char limit of this post. My equivalent rest class requires 75% less code and it manages 90% of what i need, while my GraphQl class is only built to manage a few endpoints (mostly files and translations) along with the new products integrations which is not working well enough to use in production.
By the way the productsCount issue isnât some trivial problem. Shopify built an api for an e-commerce platform that wasnât able to retrieve an accurate count of products FOR YEARS. This is indicative of some larger systemic problem.
/**
* Convert camelCase to snake_case
*
* @param string $string The string to convert
* @return string
*/
public function camelToSnake($string) {
return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $string));
}
/**
* Convert snake_case to camelCase
*
* @param string $string The string to convert
* @param bool $capitalizeFirst Whether to capitalize the first character (pascalCase)
* @return string
*/
public function snakeToCamel($string, $capitalizeFirst = false) {
$result = str_replace('_', '', ucwords($string, '_'));
if (!$capitalizeFirst) {
$result = lcfirst($result);
}
return $result;
}
/**
* Convert GraphQL response data to REST-like format
* Handles both edges/node and nodes patterns
* Recursively converts all keys from camelCase to snake_case
*/
public function convertDataToRest($data, $snake_case = true) {
if (empty($data) || !is_array($data)) {
return []; // $data;
}
# If we need to remove the wrapper key (e.g. 'products', 'collections', etc.)
if (count($data) === 1) {
$data = reset($data);
}
# Helper function to extract and convert node data
$extractNodeData = function($item) use ($snake_case) {
# If this is an edge, get the node
if (isset($item['node'])) {
$item = $item['node'];
}
# Return strings as-is
if (is_string($item)) {
return $item;
}
# Convert keys from camelCase to snake_case recursively
$converted = [];
foreach ($item as $key => $value) {
# Skip pageInfo and internal GraphQL fields
if ($key === 'pageInfo' || $key === '__typename') {
continue;
}
# Special hack for image alt texts
if ($key === 'altText') {
$converted['alt'] = $value;
unset($item['altText']);
continue;
}
# Convert the current key
if ($snake_case) {
$snakeKey = $this->camelToSnake($key);
} else {
$snakeKey = $key;
}
# Handle nested arrays recursively
if (is_array($value)) {
if ($snakeKey === 'image') {
# Special handling for image data
$value = $this->convertMediaImageToRest($value);
} else if (isset($value['edges'])) {
# If this has edges, extract the nodes
$value = array_map(function($edge) use ($snake_case) {
return $this->convertDataToRest($edge['node'], $snake_case);
}, $value['edges']);
} else if (isset($value['nodes'])) {
# If this has nodes, process them directly
$value = array_map(function($node) use ($snake_case) {
return $this->convertDataToRest($node, $snake_case);
}, $value['nodes']);
} else {
# Convert every array element recursively
$value = array_map(function($item) use ($snake_case) {
return is_array($item) ? $this->convertDataToRest($item, $snake_case) : $item;
}, $value);
# If the array itself has keys that need converting (associative array)
if (isAssociativeArray($value)) {
$tempValue = [];
foreach ($value as $k => $v) {
$tempValue[$snake_case ? $this->camelToSnake($k) : $k] = $v;
}
$value = $tempValue;
}
}
}
$converted[$snakeKey] = $value;
# Handle ID fields - duplicate to admin_graphql_api_id and convert id to basename
if ($snakeKey === 'id') {
$converted['admin_graphql_api_id'] = $value;
$converted['id'] = getFilename($value);
}
}
return $converted;
};
# Handle both edges and nodes patterns
if (isset($data['edges'])) {
return array_map($extractNodeData, $data['edges']);
}
if (isset($data['nodes'])) {
return array_map($extractNodeData, $data['nodes']);
}
# If it's just a regular array with no edges/nodes
if (is_array($data)) {
# If every value is an array, process each one
if (count($data) > 0 && array_reduce($data, function($carry, $item) {
return $carry && is_array($item);
}, true)) {
return array_map($extractNodeData, $data);
}
# Single item
return $extractNodeData($data);
}
return [];
}
public function convertProductToGraph($rest_data) {
/// Used to update some specific fields on a product
//// body_html -> description_html
if ( isset($rest_data['body_html']) ) {
$rest_data['description_html'] = $rest_data['body_html'];
unset($rest_data['body_html']);
}
//// published -> status (ACTIVE vs DRAFT)
if ( isset($rest_data['published']) ) {
$rest_data['status'] = $rest_data['published'] ? "ACTIVE" : "DRAFT";
unset($rest_data['published']);
}
//// if handle is included, set redirect_new_handle = false
if ( isset($rest_data['handle']) ) {
$rest_data['redirect_new_handle'] = false;
}
foreach ($rest_data as $key => $value) {
# convert keys to camelCase
$graph_data[$this->snakeToCamel($key)] = $value;
}
return $graph_data;
}
# Convert data that comes from GraphQL to Rest Format - Do not include product wrapper
public function convertProductToRest($graph_data) {
if ( empty($graph_data) ) {
return [];
}
# convert keys to snake_case
$rest_data = $this->convertDataToRest($graph_data, true);
# set some specific keys
$rest_data['id'] = getFilename($graph_data['id'] ?? "");
$rest_data['admin_graphql_api_id'] = $graph_data['id'] ?? "";
$rest_data['published_status'] = $this->convertStatusToPublishedStatus($graph_data['status'] ?? "");
$rest_data['body_html'] = $graph_data['descriptionHtml'] ?? "";
# status is in lowercase in Rest api
$rest_data['status'] = strtolower($graph_data['status'] ?? "");
# tags are comma separated with a space
$rest_data['tags'] = implode(", ", $graph_data['tags'] ?? []);
# Product Category
$rest_data['product_category'] = $graph_data['productCategory']['productTaxonomyNode']['name'] ?? "";
$rest_data['product_category_full'] = $graph_data['productCategory']['productTaxonomyNode']['fullName'] ?? "";
$rest_data['product_category_id'] = $graph_data['productCategory']['productTaxonomyNode']['id'] ?? "";
# loop images and convert them
# loop variants and convert them
# remove unecessary keys
unset($rest_data['description_html']);
return $rest_data;
}
# Convert data that is in REST format to GraphQL format for updating
public function convertRestProductToGraph($data) {
/// body_html -> descriptionHtml
/// published_status -> convertPublishedStatusToStatus -> status
///
}
/**
* Parse image object from GraphQL response
*/
public function convertMediaImageToRest($node) {
if ( empty($node) || !is_array($node) || empty($node['image']['src'] ?? $node['src'] ?? null) ) {
return [];
}
return [
'id' => getFilename($node['id']) ?? null,
'admin_graphql_api_id' => $node['admin_graphql_api_id'] ?? $node['id'] ?? null,
'src' => $node['image']['src'] ?? $node['src'], /// Must be present
'width' => $node['image']['width'] ?? $node['width'] ?? null,
'height' => $node['image']['height'] ?? $node['height'] ?? null,
'alt' => $node['image']['alt'] ?? $node['alt'] ?? "", // $node['image']['altText'] ?? $node['altText'] ?? "",
'created_at' => $node['createdAt'] ?? null,
'updated_at' => $node['updatedAt'] ?? null
];
}
# Convert data that comes from GraphQL to Rest Format
public function convertVariantDataToRestFormat($data) {
}
public function convertStatusToPublishedStatus($status) {
if ( empty($status) ) {
return "";
} else if ( $status == "ACTIVE" ) {
return "published";
} else {
return "unpublished";
}
}
public function convertPublishedStatusToStatus($published_status) {
if ( $published_status == "any" ) {
return "ACTIVE,DRAFT";
} else if ( $published_status == "published" ) {
return "ACTIVE";
} else {
return "DRAFT";
}
}
/////////////////// PRODUCT OPERATIONS ///////////////////
/**
* Get standard product fields for GraphQL queries
* @return array Array of product fields
*/
public function productFieldsArray() {
return [
'id',
'descriptionHtml',
'title',
'handle',
'productType',
'tags',
'vendor',
'createdAt',
'updatedAt',
'publishedAt',
'onlineStoreUrl',
'totalInventory',
'status',
// 'defaultCursor', // A default cursor that returns the single next record, sorted ascending by ID.
'images' => [
'__args' => true,
'nodes' => [
'id',
'width',
'height',
'src',
'altText'
]
],
'options' => [
'__args' => true,
'id',
'name',
'values'
],
'variants' => [
'__args' => true,
'nodes' => [
'id',
'displayName',
'title',
'price',
'compareAtPrice',
'inventoryQuantity',
'inventoryPolicy',
'availableForSale', // Doesn't seem to be added, but no errors
'sku',
'barcode',
'position',
'unitPriceMeasurement' => [
'measuredType',
'quantityUnit',
'quantityValue',
'referenceUnit',
'referenceValue'
],
]
],
'productCategory' => [
'productTaxonomyNode' => [
'fullName',
'name',
'id'
]
]
];
}
public function variantFieldsArray() {
return [
'id',
'displayName',
'title',
'price',
'compareAtPrice',
'inventoryQuantity',
'inventoryPolicy',
'availableForSale', // Doesn't seem to be added, but no errors
'sku',
'barcode',
'position',
'unitPriceMeasurement' => [
'measuredType',
'quantityUnit',
'quantityValue',
'referenceUnit',
'referenceValue'
],
'image' => $this->mediaImageFieldsArray()['image']
];
}
public function getProduct($gid, $fields = []) {
# Define the fields to retrieve
if ( empty($fields) ) {
$fields = $this->productFieldsArray();
} else {
$fields[] = 'id';
$fields = array_values(array_unique($fields));
}
# Add the ID to the Product field
$fields['__args'] = true;
# Define query arguments (variable types)
$arguments = [
'id' => ['id' => 'ID!']
];
# Define variables (actual values)
$variables = [
'id' => $gid
];
$result = $this->callGraph(
"getProduct",
$this->queryGetItems('product', $fields, [$arguments]),
$variables
);
return $result;
}
# Get all the variants of a product
public function getProductVariants($product_id, $fields = [], $params = [], $limit = 30) {
# Define the fields to retrieve
if (empty($fields)) {
$fields = $this->variantFieldsArray();
} else {
$fields[] = 'id';
$fields = array_values(array_unique($fields));
}
# Define the query structure with variants connection
$fields = [
'nodes' => $fields
];
# Define query arguments (variable types)
$arguments = [
'first' => ['first' => 'Int!'],
'query' => ['query' => 'String!'],
'sortKey' => ['sortKey' => 'ProductVariantSortKeys!']
];
# Define variables (actual values)
$variables = [
'first' => min($limit, 250), // Shopify typically limits to 250 items per request
'query' => "product_id:" . $product_id,
'sortKey' => 'POSITION'
];
$result = $this->callGraph(
"getProductVariants",
$this->queryGetItems('productVariants', $fields, [$arguments]),
$variables
);
return $result;
}
public function updateProduct($gid, $data) {
# Convert data from REST format to GraphQL format
$input = $this->convertProductToGraph($data);
# Add the ID to the input
$input['id'] = $gid;
# Define query arguments (variable types)
$arguments = [
'input' => ['input' => 'ProductInput!']
];
# Define variables (actual values)
$variables = [
'input' => $input
];
# Define the fields to retrieve
$fields = [
'product' => $this->productFieldsArray()
];
# Call the productUpdate mutation
$result = $this->callGraph(
"updateProduct",
$this->queryMutateItem('productUpdate', $fields, [$arguments]),
$variables
);
# Return the updated product
return $result['product'] ?? [];
}
/////////////// PRODUCTS, PAGES, COLLECTIONS, ARTICLES //////////////////
/**
* Low-level API call to get items
* Currently only supports products, other item types use REST API
*/
public function getItemsRestCompatible($item_type_shopify, $since_id = null, $max_items = 1, $published = "any", $fields = [], $params = [], $name = "") {
if ($item_type_shopify !== "products") {
return [];
}
$items = [];
$limit = 100; // Conservative default
if (!$max_items) {
$max_items = 100000;
}
if ( empty($fields) ) {
$fields = $this->productFieldsArray();
} else {
$fields[] = 'id';
$fields = array_values(array_unique($fields));
}
# Just include the nodes - pageInfo will be added automatically by queryGetItems
$item_fields = [
'__args' => true,
'nodes' => $fields
];
$this->ShopifyApi->resetCursors();
$page = 0;
while ($page == 0 || ($this->ShopifyApi->cursorNext && count($items) < $max_items)) {
$page++;
$remaining = $max_items - count($items);
$maxLimit = min($remaining, $limit);
# ENABLES PAGINATION
if (!empty($params['before'])) {
$this->ShopifyApi->cursorNext = $params['before'];
$productArgs = [
'productsLimit' => ['last' => 'Int!']
];
} else {
$this->ShopifyApi->cursorNext = $params['after'] ?? $this->ShopifyApi->cursorNext;
$productArgs = [
'productsLimit' => ['first' => 'Int!']
];
}
$variables['productsLimit'] = $maxLimit;
if ($this->ShopifyApi->cursorNext && !empty($params['before'])) {
$productArgs['directionCursor'] = ['before' => 'String'];
unset($params['before']);
} else if ($this->ShopifyApi->cursorNext) {
$productArgs['directionCursor'] = ['after' => 'String'];
unset($params['after']);
} else {
# First request needs query parameters
$queryConditions = [];
# Always include status condition
$status = $this->convertPublishedStatusToStatus($published);
if (!empty($status)) {
$queryConditions[] = "(status:" . $status . ")";
}
if ($since_id) {
$queryConditions[] = "(id:>" . $since_id . ")";
}
# Add handle filter if provided
if (!empty($params['handle'])) {
$queryConditions[] = "(handle:" . $params['handle'] . ")";
}
# Add collection filter if provided
if (!empty($params['collection_id'])) {
$queryConditions[] = "(collection_id:" . $params['collection_id'] . ")";
}
if (!empty($queryConditions)) {
$productArgs['queryFilter'] = ['query' => 'String!'];
}
# Add sort parameters only on first request and only if order is specified
if (!empty($params['order'])) {
$productArgs['sortKey'] = ['sortKey' => 'ProductSortKeys!'];
$productArgs['reverse'] = ['reverse' => 'Boolean!'];
} else {
# If order is empty - force 'id asc'
$productArgs['sortKey'] = ['sortKey' => 'ProductSortKeys!'];
$productArgs['reverse'] = ['reverse' => 'Boolean!'];
}
}
# Images
if (!empty($fields['images'])) {
$imagesArgs = [
'imagesFirst' => ['first' => 'Int!']
];
$variables['imagesFirst'] = $params['images_limit'] ?? 250;
} else {
$imagesArgs = [];
}
# Options
if (!empty($fields['options'])) {
$optionArgs = [
'optionsFirst' => ['first' => 'Int!']
];
$variables['optionsFirst'] = $params['options_limit'] ?? 10;
} else {
$optionArgs = [];
}
# Variants
if (!empty($fields['variants'])) {
$variantArgs = [
'variantsFirst' => ['first' => 'Int!']
];
$variables['variantsFirst'] = $params['variants_limit'] ?? 10;
} else {
$variantArgs = [];
}
# Pagination
if ($this->ShopifyApi->cursorNext) {
$variables['directionCursor'] = $this->ShopifyApi->cursorNext;
}
# Query conditions
if (!empty($queryConditions)) {
$variables['queryFilter'] = implode(" AND ", $queryConditions);
}
# Sorting
if (!empty($params['order'])) {
$sortParts = explode(' ', $params['order']);
$variables['sortKey'] = strtoupper($this->snakeToCamel($sortParts[0]));
$variables['reverse'] = isset($sortParts[1]) && strtolower($sortParts[1]) === 'desc';
} else {
$variables['sortKey'] = "ID";
$variables['reverse'] = false;
}
$result = $this->callGraph(
"getItemsRestCompatible",
$this->queryGetItems('products', $item_fields, [$productArgs, $imagesArgs, $optionArgs, $variantArgs]),
$variables,
false
);
if (empty($result['nodes'])) {
break;
}
$items = array_merge($items, $result['nodes']);
}
return array_slice($items, 0, $max_items);
}
/////////////////// FILE OPERATIONS ///////////////////
public function mediaImageFieldsArray() {
return [
'id',
'createdAt',
'updatedAt',
'fileStatus',
'image' => [
'id',
'width',
'height',
'src',
'altText'
]
];
}
/**
* Wait for an image to be ready
*/
public function waitUntilImageReady($gid, $sleep = 2) {
$sleep_max = 10;
while ($sleep_max > 0) {
sleep($sleep);
$sleep_max--;
$result = $this->getImageById($gid);
if (!empty($result['fileStatus'])) {
if ($result['fileStatus'] === 'FAILED') {
return false;
} elseif ($result['fileStatus'] === 'PROCESSING') {
continue;
} elseif ($result['fileStatus'] === 'READY') {
if ( empty($result['image']) ) {
continue; // This can happen with UPDATES since it already exists but its not done updating
} else {
return $result;
}
}
}
return false;
}
return false;
}
/**
* Get a list of images based on specified criteria
*/
public function getImages($limit = 10, $size = 40000, $cursor = [], $filename = '', $reverse = false, $used_in = '') {
# Define the fields to retrieve
$fields = [
'nodes' => [
'... on MediaImage' => $this->mediaImageFieldsArray()
]
];
if (!empty($cursor['before'])) {
$arguments['imageLimit'] = ['last' => 'Int!'];
} else {
$arguments['imageLimit'] = ['first' => 'Int!'];
}
$variables['imageLimit'] = min($limit, 250);
if (!empty($cursor['before'])) {
$arguments['before'] = ['before' => 'String'];
$variables['before'] = $cursor['before'];
} else if (!empty($cursor['after'])) {
$arguments['after'] = ['after' => 'String'];
$variables['after'] = $cursor['after'];
} else {
$arguments['query'] = ['query' => 'String!'];
# Build query conditions
$queryConditions = ["(media_type:IMAGE)", "(status:READY)"];
if ($size > 0) {
$queryConditions[] = "(original_upload_size:>$size)";
}
if ($filename) {
$queryConditions[] = "(filename:$filename)";
}
if ($used_in) {
$queryConditions[] = "(used_in:'$used_in')";
}
# Define variables (actual values)
$variables['query'] = implode(" AND ", $queryConditions);
}
$result = $this->callGraph(
"getImages",
$this->queryGetItems('files', $fields, [$arguments]),
$variables
);
return $result['nodes'] ?? [];
}
/**
* Get a file by its GraphQL ID
* @param string $gid File GraphQL ID
* @return array|null File data or null if not found
*/
public function getImageById($gid) {
# Define the fields to retrieve
$fields = [
'... on MediaImage' => $this->mediaImageFieldsArray()
];
# Define query arguments (variable types)
$arguments = [
'id' => ['id' => 'ID!']
];
# Define variables (actual values)
$variables = [
'id' => $gid
];
$result = $this->callGraph(
"getFileById",
$this->queryGetItems('node', $fields, [$arguments]),
$variables
);
return $result;
}
/**
* Get image by filename
* @param string $filename Image filename to search for
*/
public function getImageByName($filename, $used_in = '') {
# Define the fields to retrieve
$fields = [
'nodes' => [
'... on MediaImage' => $this->mediaImageFieldsArray()
]
];
# Define query arguments (variable types)
$arguments = [
'query' => ['query' => 'String!'],
'first' => ['first' => 'Int!']
];
$queryConditions = ["(media_type:IMAGE)", "(status:READY)"];
$queryConditions[] = "(filename:$filename)";
if ($used_in) {
$queryConditions[] = "(used_in:'$used_in')";
}
# Define variables (actual values)
$variables = [
'query' => implode(" AND ", $queryConditions),
'first' => 200
];
$result = $this->getAllItems(
"getImageByName",
$this->queryGetItems('files', $fields, [$arguments]),
$variables
);
# Search for the image with the exact filename
foreach ($result as $item) {
if (!empty($item['image']['src']) && getFilename($item['image']['src']) === $filename) {
return $item;
}
}
return [];
}
/**
* Create a new image
*/
public function addImage($url, $alt = "") {
# Define the fields to retrieve
$fields = [
'files' => [
'... on MediaImage' => $this->mediaImageFieldsArray()
]
];
# Define query arguments (variable types)
$arguments = [
'files' => ['files' => '[FileCreateInput!]!']
];
# Define variables (actual values)
$variables = [
'files' => [
[
'alt' => $alt,
'originalSource' => $url
]
]
];
$result = $this->callGraph(
"addImage",
$this->queryMutateItem('fileCreate', $fields, [$arguments]),
$variables
);
if (!empty($result['files'][0]['id'])) {
return $this->waitUntilImageReady($result['files'][0]['id']);
}
return null;
}
/**
* Update an existing image
*/
public function updateImage($gid, $src = null, $alt = null) {
# Define the fields to retrieve
$fields = [
'files' => [
'... on MediaImage' => $this->mediaImageFieldsArray()
]
];
# Define query arguments (variable types)
$arguments = [
'files' => ['files' => '[FileUpdateInput!]!']
];
# Build update input, only including non-null fields
$updateInput = ['id' => $gid];
if ($src !== null) {
$updateInput['previewImageSource'] = $src;
}
if ($alt !== null) {
$updateInput['alt'] = $alt;
}
# Define variables (actual values)
$variables = [
'files' => [$updateInput]
];
$result = $this->callGraph(
"updateImage",
$this->queryMutateItem('fileUpdate', $fields, [$arguments]),
$variables
);
if (!empty($result['files'][0]['id'])) {
if ($src !== null) {
return $this->waitUntilImageReady($result['files'][0]['id']);
} else {
return $result['files'][0];
}
}
return null;
}
/////////////////// QUERIES ///////////////////
public function cursorFieldsArray() {
return [
'hasNextPage',
'hasPreviousPage',
'startCursor',
'endCursor'
];
}
/**
* Check if any field in the array has __args
*/
private function hasArguments($fields) {
if (!is_array($fields)) {
return false;
}
foreach ($fields as $key => $value) {
if ($key === '__args') {
return true;
}
if (is_array($value) && $this->hasArguments($value)) {
return true;
}
}
return false;
}
/**
* Build a GraphQL query
*/
public function queryGetItems($queryType, $fields, $arguments = []) {
# If no field has __args but we have arguments, add __args to root level
if (!empty($arguments) && !$this->hasArguments($fields)) {
$fields['__args'] = true;
}
# Include cursor fields for pagination only for supported query types
$nonPaginatedTypes = [
'productsCount',
'shop',
'appInstallation',
'node',
'availableLocales',
'shopLocales'
];
if (!in_array($queryType, $nonPaginatedTypes)) {
$fields['pageInfo'] = $this->cursorFieldsArray();
}
return $this->buildGraphQLQuery($queryType, $fields, $arguments);
}
/**
* Build a GraphQL mutation
*/
public function queryMutateItem($mutationType, $fields, $arguments = []) {
# For mutations, ALWAYS add __args to root level since the mutation operation takes the input
$fields['__args'] = true;
return $this->buildGraphQLQuery($mutationType, $fields, $arguments, true);
}
/**
* Build a GraphQL query or mutation
*/
private function buildGraphQLQuery($queryType, $fields, $arguments, $isMutation = false) {
# Build the variables declaration
$varDeclarations = [];
$argStrings = [];
# Process all argument levels recursively
$this->processArguments($arguments, $varDeclarations, $argStrings);
# Start building query
$operationType = $isMutation ? "mutation" : "query";
$query = empty($varDeclarations) ?
"$operationType {\n" :
"$operationType(" . implode(", ", $varDeclarations) . ") {\n";
# Wrap fields with the root query type before processing
$wrappedFields = [
$queryType => $fields
];
# Add the fields - pass full arguments array
$query .= $this->buildFields($wrappedFields, $arguments);
# Close the query
$query .= "}\n";
return $query;
}
/**
* Recursively process arguments at all levels
*/
private function processArguments($arguments, &$varDeclarations, &$argStrings, $level = 0) {
if (empty($arguments)) {
return;
}
# Process current level
if (isset($arguments[$level]) && is_array($arguments[$level])) {
foreach ($arguments[$level] as $varName => $argDef) {
foreach ($argDef as $argName => $type) {
# Add variable declaration for all levels
$varDeclarations[] = "\$$varName: $type";
# Only add argument strings for first level
if ($level === 0) {
$argStrings[] = "$argName: \$$varName";
}
}
}
}
# Process next level recursively
if (isset($arguments[$level + 1])) {
$this->processArguments($arguments, $varDeclarations, $argStrings, $level + 1);
}
}
/**
* Recursively build fields part of query
*/
private function buildFields($fields, $arguments) {
# Initialize empty string to store the generated GraphQL fields
$result = "";
# Iterate through each field in the current level
foreach ($fields as $key => $value) {
# Skip the __args flag as it's only used for argument presence detection
if ($key === '__args') continue;
# Check if the current field has nested fields (is an array)
if (is_array($value)) {
# Skip this field entirely if it requires arguments but none are provided
if (isset($value['__args']) && empty($arguments[0]) && $key !== 'nodes') {
/// exists but is empty
if (isset($arguments[0])) {
array_shift($arguments);
}
continue;
}
# Initialize array to store argument strings for this field
$argStrings = [];
# If we have arguments for this level AND this field accepts arguments
if (!empty($arguments) && isset($value['__args']) && $key !== 'nodes') {
$args = array_shift($arguments);
# Process each argument variable at this level
foreach ($args as $varName => $argDef) {
# For each argument definition, create the argument string
foreach ($argDef as $argName => $type) {
$argStrings[] = "$argName: \$$varName";
}
}
}
# Add the field name to the query
# If we have arguments, append them in parentheses
$result .= "\t\t$key" . (!empty($argStrings) ? "(" . implode(", ", $argStrings) . ")" : "") . " {\n";
# Recursively process nested fields
$result .= $this->buildFields($value, $arguments);
# Close the nested field block
$result .= "\t\t}\n";
} else {
# For scalar fields (strings), just add them with proper indentation
$result .= "\t\t$value\n";
}
}
# Return the complete fields string for this level
return $result;
}
}