Update: you may prefer my post about doing this in Laravel.
GitHub’s new Projects are not accessible via the older REST API. Working with them programmatically involves learning some GraphQL, which can be a headache, the first time you encounter it. Here’s my approach, using PHP.
Set up cURL
You can certainly use an HTTP request library, but sometimes it’s easiest to get your hands dirty with cURL. Here’s the setup:
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Accept: application/vnd.github+json',
'Authorization: Bearer '.$_ENV['GH_TOKEN']
));
// Fake user agent, to keep GitHub happy
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0');
// Dev settings (PHP built-in server can't validate)
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
The “dev settings” are there since I’ve been using PHP’s built-in webserver during development. Don’t take those into production!
Generic API class
This class provides a simple wrapper around REST API and GraphQL calls. I’m passing in the cURL handler and calling most methods statically, since I’m not yet modelling the underlying objects.
class GitHub
{
/**
* API
* Make request to GitHub using the standard REST API.
* See: https://docs.github.com/en/rest
*
* @param [CurlHandle Object] $ch - curl handle
* @param [string] $endpoint - the REST endpoint
* @return array|stdClass
*/
public static function api(\CurlHandle $ch, string $endpoint)
{
curl_setopt($ch, CURLOPT_URL, 'https://api.github.com/'.$endpoint);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
$content = curl_exec($ch);
if (curl_errno($ch)) {
die("Error:".curl_error($ch));
}
return json_decode($content);
}
/**
* GraphQL
* Make request to GitHub using the newer GraphQL API.
* See: https://docs.github.com/en/graphql/reference
* GraphQL Explorer: https://docs.github.com/en/graphql/overview/explorer
* GraphQL Formatter: https://jsonformatter.org/graphql-formatter
*
* @param CurlHandle $ch
* @param string $query - a GraphQL query string
* @param array $variables - an array of GraphQL variables
* @return StdClass
*/
public static function graphQL(\CurlHandle $ch, string $query, array $variables)
{
curl_setopt($ch, CURLOPT_URL, 'https://api.github.com/graphql');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt(
$ch,
CURLOPT_POSTFIELDS,
json_encode(['query' => $query, 'variables' => $variables])
);
$content = curl_exec($ch);
if (curl_errno($ch)) {
die("Error:".curl_error($ch));
}
return json_decode($content);
}
}
Projects class
Various simple methods for obtaining information about projects. I say “simple” but the GraphQL queries take some mastering.
class GitHubProjects
{
/**
* getFirstN - get first N projects for the GitHub organisation, ordered by
* title
*
* @param \CurlHandle $ch
* @param integer $count - number of projects to return
* @return array
*/
public static function getFirstN(\CurlHandle $ch, int $count = 20): array
{
$projects = GitHub::graphQL(
$ch,
<<<EOD
query getFirstN(\$count: Int) {
organization(login: "MyOrg") {
projectsV2(first: \$count, orderBy: {field: TITLE, direction: ASC}) {
nodes {
number
id
title
}
}
}
}
EOD,
[ "count" => $count ]
);
return $projects->data->organization->projectsV2->nodes;
}
/**
* getFirstNIssues
*
* @param \CurlHandle $ch
* @param integer $projectNumber
* @param integer $count
* @return array
*/
public static function getFirstNIssues(\CurlHandle $ch, int $projectNumber = 1, int $count = 20): array
{
$projectId = GitHubProjects::getIdByNumber($ch, $projectNumber);
$issues = GitHub::graphQL(
$ch,
<<<EOD
# Exclamation mark since projectId is required
query getFirstNIssues(\$projectId: ID!, \$count: Int) {
node(id: \$projectId) {
... on ProjectV2 {
items(first: \$count) {
nodes {
id
fieldValues(first: 8) {
nodes {
... on ProjectV2ItemFieldTextValue {
text
field {
... on ProjectV2FieldCommon {
name
}
}
}
... on ProjectV2ItemFieldDateValue {
date
field {
... on ProjectV2FieldCommon {
name
}
}
}
... on ProjectV2ItemFieldSingleSelectValue {
name
field {
... on ProjectV2FieldCommon {
name
}
}
}
}
}
content {
... on DraftIssue {
title
body
}
... on Issue {
title
assignees(first: 10) {
nodes {
login
}
}
}
... on PullRequest {
title
assignees(first: 10) {
nodes {
login
}
}
}
}
}
}
}
}
}
EOD,
[
"projectId" => $projectId,
"count" => $count
]
);
return $issues->data->node->items->nodes;
}
/**
* getByTitle - search for projects by title
*
* @param \CurlHandle $ch
* @param string $title - search for
* @return array
*/
public static function getByTitle(\CurlHandle $ch, string $title): array
{
$projects = GitHub::graphQL(
$ch,
<<<EOD
query getByName(\$title: String!) {
organization(login: "MyOrg") {
projectsV2(
first: 20
orderBy: { field: TITLE, direction: ASC }
query: \$title
) {
nodes {
number
id
title
}
}
}
}
EOD,
[ "title" => $title ]
);
return $projects->data->organization->projectsV2->nodes;
}
/**
* getIdByNumber
*
* @param \CurlHandle $ch
* @param integer $number - the integer identifier of the project
* @return string - the internal GitHub project ID (e.g. PVT_kwDOBWQiz84AH53W)
*/
private static function getIdByNumber(\CurlHandle $ch, int $number = 1)
{
$project = GitHub::graphQL(
$ch,
<<<EOD
# Exclamation mark since number is required
query getIdByNumber(\$number: Int!) {
organization(login: "MyOrg") {
projectV2(number: \$number) {
id
}
}
}
EOD,
['number' => $number]
);
return $project->data->organization->projectV2->id;
}
}
Using it
I have not included namespacing or autoloading here. In broad terms, it’s simple to use the Projects class:
$projects = Model\GitHubProjects::getByTitle($ch, 'MyProject');
if (is_array($projects) && !empty($projects)) {
$project = $projects[0];
var_dump($project);
}