Photo by Tim Mossholder on Unsplash

Screenshot Joomla Web Services GitHub GraphQL Api Mashup
Screenshot Joomla Web Services GitHub GraphQL Api Mashup



Here is the code:

<?php
declare(strict_types=1);

/**
 * Add or Edit Joomla! Articles Via API To Multiple Sites Chosen Randomly in from a predefined list Using GitHub GraphQL API
 * - When id = 0 in csv it's doing a POST. If alias exists it add a random slug at the end of your alias and do POST again
 * - When id > 0 in csv it's doing a PATCH. If alias exists it add a random slug at the end of your alias and do PATCH again
 *
 * @author        Mr Alexandre J-S William ELISÉ <This email address is being protected from spambots. You need JavaScript enabled to view it.>
 * @copyright (c) 2009 - present. Mr Alexandre J-S William ELISÉ. All rights reserved.
 * @license       GPL-2.0-and-later GNU General Public License v2.0 or later
 * @link          https://apiadept.com
 */

// Your GitHub GraphQL API endpoint url
$dataSourceUrl = 'https://api.github.com/graphql';

// Your GitHub Personal Token (classic) (CHANGE WITH YOUR OWN TOKEN)
$dataSourceToken = 'ghp_yourowngithubpersonalclassictoken';

// Your repository owner (CHANGE THIS WITH YOUR OWN)
$yourRepositoryOwner = 'alexandreelise';

// Your repository name (CHANGE THIS WITH YOUR OWN)
$yourRepositoryName = 'j4x-api-examples';

// Your Joomla! 4.x website base url
$baseUrl = [
	'app-001' => 'https://app-001.example.org',
	'app-002' => 'https://app-002.example.org',
	'app-003' => 'https://app-003.example.org',
];
// Your Joomla! 4.x Api Token (DO NOT STORE IT IN YOUR REPO USE A VAULT OR A PASSWORD MANAGER)
$token    = [
	'app-001' => 'yourownjoomlaapitoken',
	'app-002' => 'yourownjoomlaapitoken',
	'app-003' => 'yourownjoomlaapitoken',
];
$basePath = 'api/index.php/v1';


// Request timeout
$timeout = 10;

// PHP Generator to efficiently process GitHub GraphQL Api response
$generator = function (string $dataSourceResponse, array $appIndexes): Generator {
	
	if (empty($dataSourceResponse))
	{
		yield new RuntimeException('GitHub GraphQL Api response MUST NOT be empty', 422);
	}
	
	$defaultKeys = [
		'id',
		'access',
		'title',
		'alias',
		'catid',
		'articletext',
		'introtext',
		'fulltext',
		'language',
		'metadesc',
		'metakey',
		'state',
		'featured',
		'tokenindex',
	];
	
	// Assess robustness of the code by trying random key order
	//shuffle($mergedKeys);
	
	$resource = json_decode($dataSourceResponse);
	
	if ($resource === false)
	{
		yield new RuntimeException('Could not read response', 500);
	}
	
	try
	{
		$repositoryName = $resource->data->repository->name;
		$repositoryUrl  = $resource->data->repository->url;
		
		$id       = 0;
		$title    = sprintf('Stargazers of your %s GitHub repository', $repositoryName);
		$alias    = '';
		$catid    = 2;
		$language = '*';
		$metadesc = '';
		$metakey  = '';
		$state    = 1;
		$featured = 0;
		$access   = 1;
		//choosen random tokenindex to deploy result to random url matched by this tokenindex
		$tokenindex = array_rand($appIndexes);
		
		$introtext = <<<HTML
<p>Want to know who are your repo stargazers? Here they are</p>
HTML;
		
		$stargazerList = '';
		foreach ($resource->data->repository->stargazers->edges as $stargazer)
		{
			$stargazerList .= sprintf('<li>%s</li>', $stargazer->node->name);
		}
		
		$fulltext    = <<<HTML
<p>The stargazers for your GitHub repository named: <a href="/$repositoryUrl" title="Your GitHub repository $repositoryName stargazers" target="_blank" rel="noopener nofollow">$repositoryName</a></p>
<ul>
$stargazerList
</ul>
HTML;
		$articletext = <<<HTML
        $introtext
		<hr id="system-readmore" />
        $fulltext
HTML;
		
		yield json_encode(
			array_combine(
				$defaultKeys,
				[
					$id,
					$access,
					$title,
					$alias,
					$catid,
					$articletext,
					$introtext,
					$fulltext,
					$language,
					$metadesc,
					$metakey,
					$state,
					$featured,
					$tokenindex,
				]
			)
		);
	} finally
	{
		echo 'DONE processing data' . PHP_EOL;
	}
};

// This time we need endpoint to be a function to make it more dynamic
$endpoint = function (string $givenBaseUrl, string $givenBasePath, int $givenResourceId = 0): string {
	return $givenResourceId ? sprintf('%s/%s/%s/%d', $givenBaseUrl, $givenBasePath, 'content/articles', $givenResourceId)
		: sprintf('%s/%s/%s', $givenBaseUrl, $givenBasePath, 'content/articles');
};

// Process data returned by the PHP Generator
$process = function (string $givenHttpVerb, string $endpoint, string $dataString, array $headers, int $timeout, $transport) {
	curl_setopt_array($transport, [
			CURLOPT_URL            => $endpoint,
			CURLOPT_RETURNTRANSFER => true,
			CURLOPT_ENCODING       => 'utf-8',
			CURLOPT_MAXREDIRS      => 10,
			CURLOPT_TIMEOUT        => $timeout,
			CURLOPT_FOLLOWLOCATION => true,
			CURLOPT_SSLVERSION     => CURL_SSLVERSION_TLSv1_2,
			CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_2TLS,
			CURLOPT_CUSTOMREQUEST  => $givenHttpVerb,
			CURLOPT_POSTFIELDS     => $dataString,
			CURLOPT_HTTPHEADER     => $headers,
		]
	);
	
	$response = curl_exec($transport);
	// Continue even on partial failure
	if (empty($response))
	{
		throw new RuntimeException('Empty output', 422);
	}
	
	return $response;
};

// Airtable Api response (First call to GET all records of specific table)
$dataSourceHttpVerb = 'POST';
$graphQL            = <<<GRAPHQL
{"query": "query {
  repository(owner:\"$yourRepositoryOwner\", name:\"$yourRepositoryName\") {
    name
    url
    stargazerCount
    stargazers(last:10){
      edges {
        node {
          name
        }
      }
    }
  }
}"}
GRAPHQL;

$dataSourceDataString = str_replace(["\n", "\r", "\t"], '', trim($graphQL));

$dataSourceHeaders = [
	'Accept: application/json',
	'Accept-Encoding: deflate, gzip, br',
	'Content-Type: application/json',
	'Connection: keep-alive',
	'User-Agent: Alexeli/1.0.0',
	sprintf('Content-Length: %d', mb_strlen($dataSourceDataString)),
	sprintf('Authorization: Bearer %s', trim($dataSourceToken)),
];
$dataSourceTimeout = 3;

$dataSourceTransport = curl_init();
try
{
	$dataSourceResponse = $process($dataSourceHttpVerb, $dataSourceUrl, $dataSourceDataString, $dataSourceHeaders, $dataSourceTimeout, $dataSourceTransport);
	$streamData         = $generator($dataSourceResponse, $baseUrl);
	
	$storage = [];
	foreach ($streamData as $dataString)
	{
		if (!is_string($dataString))
		{
			continue;
		}
		$curl = curl_init();
		try
		{
			$decodedDataString = json_decode($dataString);
			if ($decodedDataString === false)
			{
				continue;
			}
			
			// HTTP request headers
			$headers = [
				'Accept: application/vnd.api+json',
				'Content-Type: application/json',
				'Content-Length: ' . mb_strlen($dataString),
				sprintf('X-Joomla-Token: %s', trim($token[$decodedDataString->tokenindex])),
			];
			
			// Article primary key. Usually 'id'
			$pk     = (int) $decodedDataString->id;
			$output = $process($pk ? 'PATCH' : 'POST', $endpoint($baseUrl[$decodedDataString->tokenindex], $basePath, $pk), $dataString, $headers, $timeout, $curl);
			
			$decodedJsonOutput = json_decode($output);
			
			// don't show errors, handle them gracefully
			if (isset($decodedJsonOutput->errors))
			{
				// If article is potentially a duplicate (already exists with same alias)
				$storage[] = ['mightExists' => $decodedJsonOutput->errors[0]->code === 400, 'decodedDataString' => $decodedDataString];
				continue;
			}
			echo $output . PHP_EOL;
		}
		catch (Throwable $streamDataThrowable)
		{
			echo $streamDataThrowable->getMessage() . PHP_EOL;
			continue;
		} finally
		{
			curl_close($curl);
		}
	}
// Handle errors and retries
	foreach ($storage as $item)
	{
		$storageCurl = curl_init();
		try
		{
			if ($item['mightExists'])
			{
				$pk                               = (int) $item['decodedDataString']->id;
				$item['decodedDataString']->alias = sprintf('%s-%s', $item['decodedDataString']->alias, bin2hex(random_bytes(4)));
				// No need to do another json_encode anymore
				$dataString = json_encode($item['decodedDataString']);
				// HTTP request headers
				$headers = [
					'Accept: application/vnd.api+json',
					'Content-Type: application/json',
					'Content-Length: ' . mb_strlen($dataString),
					sprintf('X-Joomla-Token: %s', trim($token[$item['decodedDataString']->tokenindex])),
				];
				$output  = $process($pk ? 'PATCH' : 'POST', $endpoint($baseUrl[$item['decodedDataString']->tokenindex], $basePath, $pk), $dataString, $headers, $timeout, $storageCurl);
				echo $output . PHP_EOL;
			}
		}
		catch (Throwable $storageThrowable)
		{
			echo $storageThrowable->getMessage() . PHP_EOL;
			continue;
		} finally
		{
			curl_close($storageCurl);
		}
	}
}
catch (Throwable $e)
{
	echo $e->getMessage() . PHP_EOL;
} finally
{
	curl_close($dataSourceTransport);
}

For your convenience your can see the code on GitHub
  • About the author:

    Hi! Alex here,

    Joomla Developer since 2009, focused on Web Services since 2017,

    first in Joomla! 3 with jBackend and com_api

    More recently Web Services in Joomla! 4.x core.

    I like paperback format books, challenging projects and defensive cybersecurity "Blue Team".

    That's all for now. Have a delightful day.