GitHub Service: Implementation
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeNow that we have an idea of what we need the GithubService
to do, let's add the logic inside that will fetch the issues from the dino-park
repository using GitHub's API.
Add the client and make a request
To make HTTP requests, at your terminal, install Symfony's HTTP Client with:
composer require symfony/http-client
Inside of GithubService
, instantiate an HTTP client with $client = HttpClient::create()
. To make a request, call $client->request()
. This needs 2 things. 1st: what HTTP method to use, like GET
or POST
. In this case, it should be GET
. 2nd: the URL, which I'll paste in. This will fetch all of the "issues" from the dino-park
repository via GitHub's API.
// ... lines 1 - 5 | |
use Symfony\Component\HttpClient\HttpClient; | |
class GithubService | |
{ | |
public function getHealthReport(string $dinosaurName): HealthStatus | |
{ | |
// ... lines 12 - 13 | |
$client = HttpClient::create(); | |
$response = $client->request( | |
method: 'GET', | |
url: 'https://api.github.com/repos/SymfonyCasts/dino-park/issues' | |
); | |
// ... lines 20 - 27 | |
} | |
} |
Parse the HTTP Response
Ok, now what? Looking back at the dino-park
repo, GitHub will return a JSON response that contains the issues we see here. Each issue has a title with a dino's name and if the issue has a label attached to it, we'll get that back too. So, set $client->request()
to a new $response
variable. Then, below, foreach()
over $response->toArray()
as an $issue
. The cool thing about using Symfony's HTTP Client is that we don't have to bother transforming the JSON from GitHub into an array - toArray()
does that heavy lifting for us. Inside this loop, check if the issue title contains the $dinosaurName
. So if (str_contains($issue['title'],
$dinosaurName)) then we'll // Do Something
with that issue.
// ... lines 1 - 5 | |
use Symfony\Component\HttpClient\HttpClient; | |
class GithubService | |
{ | |
public function getHealthReport(string $dinosaurName): HealthStatus | |
{ | |
// ... lines 12 - 13 | |
$client = HttpClient::create(); | |
$response = $client->request( | |
method: 'GET', | |
url: 'https://api.github.com/repos/SymfonyCasts/dino-park/issues' | |
); | |
foreach ($response->toArray() as $issue) { | |
if (str_contains($issue['title'], $dinosaurName)) { | |
} | |
} | |
// ... lines 26 - 27 | |
} | |
} |
At this point, we've found the issue for our dinosaur. Woo! Now we need to loop over each label to see if we can find the health status. To help, I'll paste in a private method: you can copy this from the code block on this page.
// ... lines 1 - 4 | |
use App\Enum\HealthStatus; | |
// ... lines 6 - 7 | |
class GithubService | |
{ | |
// ... lines 10 - 29 | |
private function getDinoStatusFromLabels(array $labels): HealthStatus | |
{ | |
$status = null; | |
foreach ($labels as $label) { | |
$label = $label['name']; | |
// We only care about "Status" labels | |
if (!str_starts_with($label, 'Status:')) { | |
continue; | |
} | |
// Remove the "Status:" and whitespace from the label | |
$status = trim(substr($label, strlen('Status:'))); | |
} | |
return HealthStatus::tryFrom($status); | |
} | |
} |
This takes an array of labels... and when it finds one that starts with Status:
, it returns the correct HealthStatus
enum based on that label.
Now instead of // Do Something
, say $health = $this->getDinoStatusFromLabels()
and pass the labels with $issue['labels']
.
// ... lines 1 - 5 | |
use Symfony\Component\HttpClient\HttpClient; | |
class GithubService | |
{ | |
public function getHealthReport(string $dinosaurName): HealthStatus | |
{ | |
// ... lines 12 - 13 | |
$client = HttpClient::create(); | |
$response = $client->request( | |
method: 'GET', | |
url: 'https://api.github.com/repos/SymfonyCasts/dino-park/issues' | |
); | |
foreach ($response->toArray() as $issue) { | |
if (str_contains($issue['title'], $dinosaurName)) { | |
$health = $this->getDinoStatusFromLabels($issue['labels']); | |
} | |
} | |
// ... lines 26 - 27 | |
} | |
// ... lines 30 - 49 |
And now we can return $health
. But... what if an issue doesn't have a health status label? Hmm... at the beginning of this method, set the default $health
to HealthStatus::HEALTHY
- because GenLab would never forget to put a Sick
label on a dino that isn't feeling well.
// ... lines 1 - 7 | |
class GithubService | |
{ | |
public function getHealthReport(string $dinosaurName): HealthStatus | |
{ | |
$health = HealthStatus::HEALTHY; | |
$client = HttpClient::create(); | |
$response = $client->request( | |
method: 'GET', | |
url: 'https://api.github.com/repos/SymfonyCasts/dino-park/issues' | |
); | |
foreach ($response->toArray() as $issue) { | |
if (str_contains($issue['title'], $dinosaurName)) { | |
$health = $this->getDinoStatusFromLabels($issue['labels']); | |
} | |
} | |
return $health; | |
} | |
// ... lines 29 - 49 |
Hmm... Welp, I think we did it! Let's run our tests to be sure.
./vendor/bin/phpunit
And... Wow! We have 8 tests, 11 assertions, and they're all passing! Shweeet!
Log all of our requests
One last challenge! To help debugging, I want to log a message each time we make a request to the GitHub API.
No problem! We just need to get the logger service. Add a constructor with private LoggerInterface $logger
to add an argument and property all at once. Right after we call the request()
method, add $this->logger->info()
and pass Request Dino Issues
for the message and also an array with extra context. How about a dino
key set to $dinosaurName
and responseStatus
to $response->getStatusCode()
.
// ... lines 1 - 5 | |
use Psr\Log\LoggerInterface; | |
// ... lines 7 - 8 | |
class GithubService | |
{ | |
public function __construct(private LoggerInterface $logger) | |
{ | |
} | |
public function getHealthReport(string $dinosaurName): HealthStatus | |
{ | |
// ... lines 17 - 25 | |
$this->logger->info('Request Dino Issues', [ | |
'dino' => $dinosaurName, | |
'responseStatus' => $response->getStatusCode(), | |
]); | |
// ... lines 30 - 37 | |
} | |
// ... lines 39 - 57 | |
} |
Cool! That shouldn't have broken anything in our class, but let's run the tests to be sure:
./vendor/bin/phpunit
And... Ouch! We did break something!
Too few arguments passed to the constructor in GithubService. 0 passed 1 expected.
Of course! When we added the LoggerInterface
argument to GithubService
, we never updated our test to pass that in. I'll show you how we can do that next using one of PHPUnit's super abilities: mocking.
Wouldn't it be better to use the Symfony\Contracts\HttpClient\HttpClientInterface? If not, what would be the disadvantages?