Dynamic Repositories and Other Geekery


In this final video in this series we are going to continue on with converting the Repositories block from static data, into real dynamic data from the GitHub API response.

The concepts we are going to use are the same as those we covered in the previous video. We are going to continue using Guzzle for our HTTP requests, and extract the code from the Controller into the more modularised, specialised Service class.

By the end of the previous video we had created the GitHubApi service class, so the first step is to create another new method inside this class, which we will call getRepos:

<?php

namespace AppBundle\Service;

use GuzzleHttp\Client;

class GitHubApi
{
    public function getProfile($username)
    {
        /** method from previous video */
    }

    public function getRepositories($username)
    {
        $client = new Client();
        /** @var $response \Psr\Http\Message\ResponseInterface */
        $response = $client->request('GET', 'https://api.github.com/users/' . $username . '/repos');

        $data = json_decode($response->getBody()->getContents(), true);

        return [
            'repo_count' => count($data),
            'most_stars' => array_reduce($data, function ($mostStars, $currentRepo) {
                return $currentRepo['stargazers_count'] > $mostStars ? $currentRepo['stargazers_count'] : $mostStars;
            }, 0),
            'repos' => $data,
        ];
    }
}

This code can certainly be improved, but it gets the job done for an app of this size. Let's discuss what it does first, and then look at ways it could be improved.

If you are new to Object Oriented Programming (OOP), the first line may be enough to confuse you:

$client = new Client();

What is this Client, and where the heck is it coming from?

Notice the use GuzzleHttp\Client; statement at the top of this class. This can be thought of as being very similar to include or require / require_once from 'the olden days'. A class can use other classes. In this instance we are going to use the Guzzle Http Client to enable our code to talk to other servers.

We could even get rid of the use statement and have this inline:

$client = new GuzzleHttp\Client();

But we also new up an instance of Client inside the getProfile method, so we'd need to change that line too. Take note of this, as this is something we could definitely improve upon, and will discuss again shortly.

/** @var $response \Psr\Http\Message\ResponseInterface */
$response = $client->request('GET', 'https://api.github.com/users/' . $username . '/repos');

Now we have this Guzzle Client we can go ahead and make a request with it. There's two immediate ways to figure out how to make a request.

First, we could look at the code for the GuzzleHttp\Client interface, and the provided implementation of the class, and figure out what methods need what properties in order to make a request.

Or second, we could RTFM. The manual is much easier, in my opnion, so I'd suggest you start there :)

Making a request is in essence, very similar to what your browser would do. You've likely heard of GET and POST requests. Most web requests are GET requests - we want to get something from a server (a web page for example). And if we want to send in some data - maybe a form - we can POST that data in to the server.

Guzzle follows this same paradigm. We tell it what verb we want (GET, POST, PUT, DELETE, etc), and then give it a URL to talk too. Our URL can be a variable, an plain old string, some concatanated inline string like I have used...

Guzzle will combine the two and make the request. The response will be returned, and in our case, stored on a variable called $response. So far, so normal.

Now, as we covered in the previous video, this $response variable will be a stream. And a stream is initially very confusing to work with. Just gimme mah data!

Thankfully, we discussed this in the previous video. To get access to the real data we need to call:

$response->getBody()->getContents()

This is (hopefully) going to be JSON, which PHP can't immediately work with. Before it can work with the data, we need to decode it to an array:

$data = json_decode($response->getBody()->getContents(), true);

I say 'hopefully' as I am not handling any error situations in this code. Another point to discuss below.

The other question I imagine you have is:

What is the purpose of:

/** @var $response \Psr\Http\Message\ResponseInterface */

That's a great question. We can provide a comment like this to help PHPStorm (and possibly other IDEs, but I only use PHPStorm) to figure our what methods should be available on the $response variable. Remember, we are working with classes here, so the $response variable is likely to be some sort of class.

It is a class, it's a class that implements \Psr\Http\Message\ResponseInterface. By telling PHPStorm that $response is some implementation of the ResponseInterface, PHPStorm will then give us code completion / intellisense-like hints on what methods we can use on that object. Awesome.

Now that we have the response from our GitHub API query, and we have decoded it from JSON into a plain old PHP array, we can begin to update our static / mocked result into a lovely dynamic ensemble.

Previously, we had this:

$templateData = [
    'repo_count' => 100,
    'most_stars' => 50,
    'repos' => [
        [
            'url' => 'https://codereviewvideos.com',
            'name' => 'Code Review Videos',
            'description' => 'some repo description',
            'stargazers_count' => '999',
        ],
        [
            'url' => 'http://bbc.co.uk',
            'name' => 'The BBC',
            'description' => 'not a real repo',
            'stargazers_count' => '666',
        ],
        [
            'url' => 'http://google.co.uk',
            'name' => 'Google',
            'description' => 'another fake repo description',
            'stargazers_count' => '333',
        ],
    ]
];

And now we want the same data structure, but we want the data to be dynamic - as in, the real data from the GitHub API response.

But here we hit upon a new problem.

Repo Count, and Most Stars, are not values returned directly from GitHub's API. That is to say, these are not two fields we can read from the JSON data, and simply display.

Instead, we must write our own logic to determine these value. Which is good news, as this allows us to exercise our development chops :)

Getting repo_count should be really easy. We know we have an array of data containing all our repositories. So, we can simply count the number of elements in the array, and et voila! That's our repo_count:

'repo_count' => count($data),

Getting the repository with the most stars is a little trickier. However, there is a PHP function that will allow us to 'inline' this calculation: array_reduce. To quote direct from the PHP Docs:

array_reduce — Iteratively reduce the array to a single value using a callback function

Uh huh.

In laymans terms this means to loop through the given array ($data in our case), and apply a function we supply to figure out what single piece of data to return.

In other words, we are going to loop through $data, look at each individual item inside $data (which will be an array containing the individual repository information), grab the stargazers_count from the array currently being looped over, and if this stargazers_count is greater than the largest stargazers_count we already know about, then make this stargazers_count the current highest. Then repeat until there are no more items left in the array.

If you're still unsure on this, watch the video where it is explained more visually.

'most_stars' => array_reduce($data, function ($mostStars, $currentRepo) {
    return $currentRepo['stargazers_count'] > $mostStars ? $currentRepo['stargazers_count'] : $mostStars;
}, 0),

And finally, to remaining in line with our mocked data, we need the list of repos. This one is super easy as we already have those repos: $data!

'repos' => $data,`

Meaning our finished function looks like this :

public function getRepositories($username)
{
    $client = new Client();
    /** @var $response \Psr\Http\Message\ResponseInterface */
    $response = $client->request('GET', 'https://api.github.com/users/' . $username . '/repos');

    $data = json_decode($response->getBody()->getContents(), true);

    return [
        'repo_count' => count($data),
        'most_stars' => array_reduce($data, function ($mostStars, $currentRepo) {
            return $currentRepo['stargazers_count'] > $mostStars ? $currentRepo['stargazers_count'] : $mostStars;
        }, 0),
        'repos' => $data,
    ];
}

Much like before, we can now update the GitHutController to separate the repos out into their own action:

<?php

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class GithutController extends Controller
{
    /**
     * @Route("/{username}", name="githut", defaults={ "username": "codereviewvideos" })
     */
    public function githutAction(Request $request, $username)
    {
        return $this->render('githut/index.html.twig', [
            'username' => $username
        ]);
    }

    /**
     * @Route("/profile/{username}", name="profile")
     */
    public function profileAction(Request $request, $username)
    {
        $profile = $this->get('github_api')->getProfile($username);

        return $this->render('githut/profile.html.twig', $profile);
    }

    /**
     * @Route("/repos/{username}", name="repos")
     */
    public function reposAction(Request $request, $username)
    {
        $repos = $this->get('github_api')->getRepositories($username);

        return $this->render('githut/repos.html.twig', $repos);
    }
}

And update the Twig templates as needed:

<!-- app/Resources/views/githut/repos.html.twig --> 
<div class="row">
    <div class="col-sm-12">

        <div class="well">

            Important Stats

            <div class="pull-right">
                <span class="label label-primary">Repositories: {{ repo_count }}</span>
                <span class="label label-success">Most Stars: {{ most_stars }}</span>
            </div>

        </div>

        <div class="panel panel-default">
            <div class="panel-heading">
                <h3 class="panel-title">Repo List</h3>
            </div>

            <ul class="list-group">
                {% for repo in repos %}
                <li class="list-group-item">{% include ':githut:repo.html.twig' with repo %}</li>
                {% endfor %}
            </ul>
        </div>

    </div>
</div>

The individual repo template:

<!-- app/Resources/views/githut/repo.html.twig --> 
<a href="{{ repo.url }}">{{ repo.name }}</a>
<br />
<span class="small">{{ repo.description }}</span>
<span class="pull-right">
    {{ repo.stargazers_count }} <i class="glyphicon glyphicon-star"></i>
</span>

And lastly the index template:

<!-- app/Resources/views/githut/index.html.twig -->

{% extends 'base.html.twig' %}

{% block body %}
    <div class="col-sm-3">
        {{ render (controller('AppBundle:Githut:profile', { 'username': username })) }}
    </div>

    <div class="col-sm-9">
        {{ render (controller('AppBundle:Githut:repos', { 'username': username })) }}
    </div>
{% endblock %}

Potential Improvements

This was a beginners guide. Often the ways shown to beginners are not the same ways used on larger projects.

Let's think about ways we could improve this code.

Dependency Injection

Symfony does a great job of Dependency Injection. Dependency Injection is a way to centralise / maintain better control of the way objects are created within your application.

In both methods of our GitHubApi service, we did the following:

$client = new Client();

On larger scale applications, this will come back to haunt you. It will also make testing this method a real pain.

Instead, we should think about injecting the client into our service.

Error Handling

We make no effort to handle situations where the returned data is invalid, or that GitHub's API is down / offline / uncontactable.

Any time we are doing something that may fail (making a web request being a potentially unreliable thing), we need to be wrapping our code in a try / catch block.

Our entire system will fail if we get a bad response in any way. We could definitely add in if / else logic in Twig to handle situations where our arrays are empty, displaying a 'sorry' page if things went badly, rather than blowing up spectacularly.

Array Collections

As you continue learning Symfony, you are highly likely to come across Doctrine. Without diving in to Doctrine at this stage, its worth giving a heads up that you will start working with Array Collections.

The Collection interface provides us with a number of useful helper methods to make working with Arrays more enjoyable.

It could be worth wrapping our GitHub result inside an array collection, making the use of filter a little more pleasant. However, if you don't understand how array_filter works underneath, then filter is only going to confuse you further.

Wrapping Up

In this series we have learned how to use Symfony, Twig, and Guzzle to create a working GitHub profile web application.

We've covered how to use Symfony's Controllers to receive requests, hand off to services that manage our 'business logic', and then return a response that makes use of Twig to display HTML to end users.

We've covered how to nest our templates inside Twig to encourage a re-usable template structure.

We've also briefly looked at Guzzle, a library for making HTTP requests out to the web, using the response to populate our Twig templates with their respective pieces of data.

I hope you have enjoyed this getting started guide for Symfony 3, and are excited to continue learning the framework, and expanding your knowledge.

Thank you very much for viewing this course, and I truly hope you have found it useful.

Chris

Code For This Course

Get the code for this course.

Episodes