Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Now 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.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "symfony/asset": "6.1.*", // v6.1.0
        "symfony/console": "6.1.*", // v6.1.4
        "symfony/dotenv": "6.1.*", // v6.1.0
        "symfony/flex": "^2", // v2.2.3
        "symfony/framework-bundle": "6.1.*", // v6.1.4
        "symfony/http-client": "6.1.*", // v6.1.4
        "symfony/runtime": "6.1.*", // v6.1.3
        "symfony/twig-bundle": "6.1.*", // v6.1.1
        "symfony/yaml": "6.1.*" // v6.1.4
    },
    "require-dev": {
        "phpunit/phpunit": "^9.5", // 9.5.23
        "symfony/browser-kit": "6.1.*", // v6.1.3
        "symfony/css-selector": "6.1.*", // v6.1.3
        "symfony/phpunit-bridge": "^6.1" // v6.1.3
    }
}