Skip to content

Commit eb2a4c3

Browse files
authored
perf: Combine first two requests and refactor (DenverCoder1#409)
1 parent e3e003d commit eb2a4c3

File tree

1 file changed

+61
-110
lines changed

1 file changed

+61
-110
lines changed

src/stats.php

Lines changed: 61 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
declare(strict_types=1);
44

55
/**
6-
* Build a query for a contribution graph
6+
* Build a GraphQL query for a contribution graph
77
*
88
* @param string $user GitHub username to get graphs for
99
* @param int $year Year to get graph for
@@ -17,8 +17,8 @@ function buildContributionGraphQuery(string $user, int $year)
1717
return "query {
1818
user(login: \"$user\") {
1919
contributionsCollection(from: \"$start\", to: \"$end\") {
20+
contributionYears
2021
contributionCalendar {
21-
totalContributions
2222
weeks {
2323
contributionDays {
2424
contributionCount
@@ -32,49 +32,60 @@ function buildContributionGraphQuery(string $user, int $year)
3232
}
3333

3434
/**
35-
* Get all HTTP request responses for user's contributions
35+
* Execute multiple requests with cURL and handle GitHub API rate limits and errors
3636
*
3737
* @param string $user GitHub username to get graphs for
38+
* @param array<int> $years Years to get graphs for
3839
*
39-
* @return array<stdClass> List of contribution graph response objects
40+
* @return array<stdClass> List of GraphQL response objects
4041
*/
41-
function getContributionGraphs(string $user): array
42+
function executeContributionGraphRequests(string $user, array $years): array
4243
{
43-
// Get the years the user has contributed
44-
$contributionYears = getContributionYears($user);
45-
// build a list of individual requests
4644
$tokens = [];
4745
$requests = [];
48-
foreach ($contributionYears as $year) {
49-
// create query for year
50-
$query = buildContributionGraphQuery($user, $year);
51-
// create curl request
46+
// build handles for each year
47+
foreach ($years as $year) {
5248
$tokens[$year] = getGitHubToken();
49+
$query = buildContributionGraphQuery($user, $year);
5350
$requests[$year] = getGraphQLCurlHandle($query, $tokens[$year]);
5451
}
5552
// build multi-curl handle
5653
$multi = curl_multi_init();
57-
foreach ($requests as $request) {
58-
curl_multi_add_handle($multi, $request);
54+
foreach ($requests as $handle) {
55+
curl_multi_add_handle($multi, $handle);
5956
}
6057
// execute queries
6158
$running = null;
6259
do {
6360
curl_multi_exec($multi, $running);
6461
} while ($running);
65-
// collect responses from last to first
66-
$response = [];
67-
foreach ($requests as $year => $request) {
68-
$contents = curl_multi_getcontent($request);
62+
// collect responses
63+
$responses = [];
64+
foreach ($requests as $year => $handle) {
65+
$contents = curl_multi_getcontent($handle);
6966
$decoded = is_string($contents) ? json_decode($contents) : null;
70-
// if response is empty or invalid, retry request one time
71-
if (empty($decoded) || empty($decoded->data)) {
72-
// if rate limit is exceeded, don't retry with same token
67+
// if response is empty or invalid, retry request one time or throw an error
68+
if (empty($decoded) || empty($decoded->data) || !empty($decoded->errors)) {
7369
$message = $decoded->errors[0]->message ?? ($decoded->message ?? "An API error occurred.");
70+
$error_type = $decoded->errors[0]->type ?? "";
71+
// Missing SSL certificate
72+
if (curl_errno($handle) === 60) {
73+
throw new AssertionError("You don't have a valid SSL Certificate installed or XAMPP.", 500);
74+
}
75+
// Other cURL error
76+
elseif (curl_errno($handle)) {
77+
throw new AssertionError("cURL error: " . curl_error($handle), 500);
78+
}
79+
// GitHub API error - Not Found
80+
elseif ($error_type === "NOT_FOUND") {
81+
throw new InvalidArgumentException("Could not find a user with that name.", 404);
82+
}
83+
// if rate limit is exceeded, don't retry with same token
7484
if (str_contains($message, "rate limit exceeded")) {
7585
removeGitHubToken($tokens[$year]);
7686
}
7787
error_log("First attempt to decode response for $user's $year contributions failed. $message");
88+
// retry request
7889
$query = buildContributionGraphQuery($user, $year);
7990
$token = getGitHubToken();
8091
$request = getGraphQLCurlHandle($query, $token);
@@ -90,14 +101,36 @@ function getContributionGraphs(string $user): array
90101
continue;
91102
}
92103
}
93-
array_unshift($response, $decoded);
104+
$responses[$year] = $decoded;
94105
}
95106
// close the handles
96107
foreach ($requests as $request) {
97-
curl_multi_remove_handle($multi, $request);
108+
curl_multi_remove_handle($multi, $handle);
98109
}
99110
curl_multi_close($multi);
100-
return $response;
111+
return $responses;
112+
}
113+
114+
/**
115+
* Get all HTTP request responses for user's contributions
116+
*
117+
* @param string $user GitHub username to get graphs for
118+
*
119+
* @return array<stdClass> List of contribution graph response objects
120+
*/
121+
function getContributionGraphs(string $user): array
122+
{
123+
// get the list of years the user has contributed and the current year's contribution graph
124+
$currentYear = intval(date("Y"));
125+
$responses = executeContributionGraphRequests($user, [$currentYear]);
126+
$contributionYears = $responses[$currentYear]->data->user->contributionsCollection->contributionYears;
127+
// remove the current year from the list since it's already been fetched
128+
$contributionYears = array_filter($contributionYears, function ($year) use ($currentYear) {
129+
return $year !== $currentYear;
130+
});
131+
// get the contribution graphs for the previous years
132+
$responses += executeContributionGraphRequests($user, $contributionYears);
133+
return $responses;
101134
}
102135

103136
/**
@@ -127,7 +160,7 @@ function getGitHubTokens()
127160
/**
128161
* Get a token from the token pool
129162
*
130-
* @throws AssertionError if no tokens are available
163+
* @return string GitHub token
131164
*/
132165
function getGitHubToken()
133166
{
@@ -139,6 +172,8 @@ function getGitHubToken()
139172
* Remove a token from the token pool
140173
*
141174
* @param string $token Token to remove
175+
*
176+
* @throws AssertionError if no tokens are available after removing the token
142177
*/
143178
function removeGitHubToken(string $token)
144179
{
@@ -183,94 +218,10 @@ function getGraphQLCurlHandle(string $query, string $token)
183218
return $ch;
184219
}
185220

186-
/**
187-
* Create a POST request to GitHub's GraphQL API
188-
*
189-
* @param string $query GraphQL query
190-
* @param string $token GitHub token to use for the request
191-
*
192-
* @return stdClass An object from the json response of the request
193-
*
194-
* @throws AssertionError If SSL verification fails
195-
*/
196-
function fetchGraphQL(string $query, string $token): stdClass
197-
{
198-
$ch = getGraphQLCurlHandle($query, $token);
199-
$response = curl_exec($ch);
200-
curl_close($ch);
201-
$decoded = is_string($response) ? json_decode($response) : null;
202-
// handle curl errors
203-
if ($response === false || $decoded === null || curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 400) {
204-
$message = $decoded->errors[0]->message ?? ($decoded->message ?? "");
205-
if (str_contains($message, "rate limit exceeded")) {
206-
removeGitHubToken($token);
207-
}
208-
// set response code to curl error code
209-
http_response_code(curl_getinfo($ch, CURLINFO_HTTP_CODE));
210-
// Missing SSL certificate
211-
if (str_contains(curl_error($ch), "unable to get local issuer certificate")) {
212-
throw new AssertionError("You don't have a valid SSL Certificate installed or XAMPP.", 400);
213-
}
214-
// Handle errors such as "Bad credentials"
215-
if ($message) {
216-
throw new AssertionError("Error: $message \n<!-- $response -->", 401);
217-
}
218-
// Handle curl errors
219-
if (curl_errno($ch)) {
220-
throw new AssertionError("cURL error: " . curl_error($ch) . "\n<!-- $response -->", 500);
221-
}
222-
throw new AssertionError("An error occurred when getting a response from GitHub.\n<!-- $response -->", 502);
223-
}
224-
return $decoded;
225-
}
226-
227-
/**
228-
* Get the years the user has contributed
229-
*
230-
* @param string $user GitHub username to get years for
231-
*
232-
* @return array List of years the user has contributed
233-
*
234-
* @throws InvalidArgumentException If the user doesn't exist or there is an error
235-
*/
236-
function getContributionYears(string $user): array
237-
{
238-
$query = "query {
239-
user(login: \"$user\") {
240-
contributionsCollection {
241-
contributionYears
242-
}
243-
}
244-
}";
245-
try {
246-
$response = fetchGraphQL($query, getGitHubToken());
247-
} catch (AssertionError $e) {
248-
// retry once if an error occurred
249-
error_log("An error occurred getting contribution years for $user: " . $e->getMessage());
250-
$response = fetchGraphQL($query, getGitHubToken());
251-
}
252-
// User not found
253-
if (!empty($response->errors)) {
254-
$type = $response->errors[0]->type ?? "";
255-
if ($type === "NOT_FOUND") {
256-
throw new InvalidArgumentException("Could not find a user with that name.", 404);
257-
}
258-
$message = $response->errors[0]->message ?? "An API error occurred.";
259-
// Other errors that contain a message field
260-
throw new InvalidArgumentException($message, 502);
261-
}
262-
// API did not return data
263-
if (!isset($response->data) && isset($response->message)) {
264-
// Other errors that contain a message field
265-
throw new InvalidArgumentException($response->message, 204);
266-
}
267-
return $response->data->user->contributionsCollection->contributionYears;
268-
}
269-
270221
/**
271222
* Get an array of all dates with the number of contributions
272223
*
273-
* @param array<string> $contributionCalendars List of GraphQL response objects
224+
* @param array<stdClass> $contributionCalendars List of GraphQL response objects
274225
*
275226
* @return array<string, int> Y-M-D dates mapped to the number of contributions
276227
*/

0 commit comments

Comments
 (0)