This class is responsible for making the HTTP calls to the github commits API. It throws an exception if there's a problem making the request, if there's a problem parsing the response, or if an error is retured in the response.

(double-click the code to select all)
<?php
namespace github;
use \Datetime;
use \Exception;
use \Settings;

/**
 * Interface for calling the GitHub API.
 * @see http://develop.github.com/
 * @author Michael Angstadt
 */
class GitHubApi {
	/**
	 * Gets the time of the last commit from a GitHub project.
	 * @param string $username the username of the project owner
	 * @param string $projectName the name of the project
	 * @return DateTime the time of the latest commit
	 * @throws Exception if there's any sort of problem making the request
	 */
	public function getLatestCommitTime($username, $projectName) {
		//the access token allows more requests to be sent per day
		//tokens can be managed here: https://github.com/settings/applications
		//see: http://developer.github.com/v3/#authentication
		$accessToken = Settings::$githubApiAccessToken;
		
		//make the request
		$context = stream_context_create([
			'http' => [
				'header' => [
					"User-Agent: http://mikeangstadt.name", //API calls require a user-agent, see: http://developer.github.com/v3/#user-agent-required 
					"Authorization: $accessToken"
				]
			]
		]);
		
		$response = file_get_contents("https://api.github.com/repos/$username/$projectName/commits", false, $context);
		
		if ($response === false){
			throw new Exception("Error contacting github.");
		}
		
		//parse the JSON
		$json = json_decode($response);
		if ($json === null){
			throw new Exception("Error parsing JSON response from github.");
		}
		if (isset($json->error)){
			throw new Exception($json->error);
		}
		
		//get the date
		$dateStr = $json[0]->commit->author->date;
		return new DateTime($dateStr);
	}
}

I use this class to cache the data I get back from github. It uses the refreshed XML attribute to determine when the most recent commit date of a project was last updated in the cache.

(double-click the code to select all)
<?php
namespace github;
use \DateTime;

/**
 * Caches the data from github.
 * @author Michael Angstadt
 */
class GitHubCache {
	/**
	 * The path to the cache file.
	 * @var string
	 */
	private $path;
	
	/**
	 * The number of seconds old data is allowed to be before it is updated.
	 * @var integer
	 */
	private $refresh;
	
	/**
	 * The XML document.
	 * @var SimpleXMLElement
	 */
	private $xml;
	
	/**
	 * True if the cache has been modified, false if not.
	 * @var boolean
	 */
	private $modified = false;
	
	/**
	 * @param string $path the path to the cache file
	 * @param integer $refresh the number of seconds old data is allowed to be before it is updated
	 */
	public function __construct($path, $refresh = 3600){
		$this->path = $path;
		$this->refresh = $refresh;
		
		if (!file_exists($path)){
			$this->xml = simplexml_load_string('<github />');
			$this->modified = true;
		} else {
			$this->xml = simplexml_load_file($path);
		}
	}

	/**
	 * Determines if a value needs updating
	 * @param array(mixed, DateTime) $data two-element array: [0] = the data, [1] = the time it was last updated
	 * @return boolean true if the data is out of date, false if it's current enough
	 */
	public function needsUpdating($data){
		if ($data == null){
			return true;
		}
		$date = $data[1];
		$diff = time() - $date->getTimestamp();
		return $diff > $this->refresh;
	}

	/**
	 * Gets the latest commit time for a project.
	 * @param string $username the project owner
	 * @param string $project the project name
	 * @return array(DateTime, DateTime) null if no data exists for the project or an array:
	 * [0] = the latest commit time for the project,
	 * [1] = the time this data was last updated in the cache
	 */
	public function getLatestCommitTime($username, $project){
		$result = $this->xml->xpath("/github/project[@name='$project' and @username='$username']");
		if (count($result) == 0){
			return null;
		} else {
			$project = $result[0];
			$commit = new DateTime((string)$project['commit']);
			$refreshed = new DateTime((string)$project['refreshed']);
			return array($commit, $refreshed);
		}
	}
	
	/**
	 * Sets the latest commit time for a project.
	 * @param string $username the project owner
	 * @param string $project the project name
	 * @param DateTime $time the latest commit time
	 */
	public function setLatestCommitTime($username, $project, DateTime $time){
		$now = new DateTime();
		$result = $this->xml->xpath("/github/project[@name='$project' and @username='$username']");
		if (count($result) == 0){
			$node = $this->xml->addChild('project');
			$node->addAttribute('name', $project);
			$node->addAttribute('username', $username);
			$node->addAttribute('commit', $time->format('Y/m/d H:i:s'));
			$node->addAttribute('refreshed', $now->format('Y/m/d H:i:s'));
		} else {
			$project = $result[0];
			$project['commit'] = $time->format('Y/m/d H:i:s');
			$project['refreshed'] = $now->format('Y/m/d H:i:s');
		}
		
		$this->modified = true;
	}
	
	/**
	 * Saves the cache to disk if it has been modified.
	 */
	public function save(){
		if ($this->modified){
			$this->xml->asXML($this->path);
		}
	}
}

The unit test class for the GitHubCache class. There is a test for the getLastestCommitTime() and setLatestCommitTime() methods, as well as a test to make sure it correctly handles saving a cache with no data. You should always include border cases like these in your unit tests. They don't happen often in a live environment, they can be tricky to code, so are more likely to contain bugs.

(double-click the code to select all)
<?php
namespace github;
use \DateTime;
use utils\TestWrapper;

/**
 * Tests the GitHubCache class.
 * @author Michael Angstadt
 */
class GitHubCacheTest extends \PHPUnit_Framework_TestCase {
	private $file = 'GitHubCacheTest.xml';
	
	public function tearDown(){
		unlink($this->file);
	}
	
	/**
	 * Tests to make sure it saves an empty cache correctly.
	 */
	public function testSaveEmpty(){
		$cache = new GitHubCache($this->file);
		$cache->save();
		$expected = "<?xml version=\"1.0\"?>\n<github/>\n";
		$actual = file_get_contents($this->file);
		$this->assertEquals($expected, $actual);
	}
	
	/**
	 * Tests the getLatestCommitTime method.
	 */
	public function testGetLatestCommitTime(){
		$xml = <<<XML
		<github>
			<project name="PHP-HAPI" username="mangstadt" commit="2011/11/27 14:22:33" refreshed="2011/11/28 09:56:11" />
			<project name="play" username="playframework" commit="2011/12/15 08:11:55" refreshed="2011/12/16 11:24:53" />
		</github>
XML;
		file_put_contents($this->file, $xml);
		$cache = new GitHubCache($this->file);
		
		$result = $cache->getLatestCommitTime('mangstadt', 'PHP-HAPI');
		$this->assertEquals(new DateTime('2011/11/27 14:22:33'), $result[0]);
		$this->assertEquals(new DateTime('2011/11/28 09:56:11'), $result[1]);
		
		$result = $cache->getLatestCommitTime('playframework', 'play');
		$this->assertEquals(new DateTime('2011/12/15 08:11:55'), $result[0]);
		$this->assertEquals(new DateTime('2011/12/16 11:24:53'), $result[1]);
		
		//non-existent projects
		$result = $cache->getLatestCommitTime('mangstadt', 'non-existent');
		$this->assertNull($result);
		$result = $cache->getLatestCommitTime('not-a-user', 'PHP-HAPI');
		$this->assertNull($result);
		$result = $cache->getLatestCommitTime('not-a-user', 'non-existent');
		$this->assertNull($result);
	}
	
	/**
	 * Tests the setLatestCommitTime method.
	 */
	public function testSetLatestCommitTime(){
		$xml = <<<XML
		<github>
			<project name="PHP-HAPI" username="mangstadt" commit="2011/11/27 14:22:33" refreshed="2011/11/28 09:56:11" />
			<project name="play" username="playframework" commit="2011/12/15 08:11:55" refreshed="2011/12/16 11:24:53" />
		</github>
XML;
		file_put_contents($this->file, $xml);
		$cache = new GitHubCache($this->file);
		
		//edit existing project
		$cache->setLatestCommitTime('mangstadt', 'PHP-HAPI', new DateTime('2011/12/27 10:22:00'));
		$result = $cache->getLatestCommitTime('mangstadt', 'PHP-HAPI');
		$this->assertEquals(new DateTime('2011/12/27 10:22:00'), $result[0]);
		$this->assertTrue(time() - $result[1]->getTimestamp() < 2);
		
		//add new project
		$cache->setLatestCommitTime('koto', 'phar-util', new DateTime('2011/12/25 11:11:11'));
		$result = $cache->getLatestCommitTime('koto', 'phar-util');
		$this->assertEquals(new DateTime('2011/12/25 11:11:11'), $result[0]);
		$this->assertTrue(time() - $result[1]->getTimestamp() < 2);
		
		//save the cache
		$cache->save();
		
		//reload it and make sure it saved everything correctly
		$cache = new GitHubCache($this->file);
		
		$result = $cache->getLatestCommitTime('mangstadt', 'PHP-HAPI');
		$this->assertEquals(new DateTime('2011/12/27 10:22:00'), $result[0]);
		$this->assertTrue(time() - $result[1]->getTimestamp() < 2);
		
		$result = $cache->getLatestCommitTime('koto', 'phar-util');
		$this->assertEquals(new DateTime('2011/12/25 11:11:11'), $result[0]);
		$this->assertTrue(time() - $result[1]->getTimestamp() < 2);
	}
}

This data access object (DAO) contains the code that makes the GitHubApi and GitHubCache classes work well together. If the data in the cache is still fresh, it uses that. If the data in the cache is stale or non-existent, then it queries the Github API. It's designed so that using a cache is optional--if none is provided, then it will always pull data from the Github API.

(double-click the code to select all)
<?php
namespace github;
use \Datetime;
use \Exception;

/**
 * Interface for calling the GitHub API.
 * @see http://develop.github.com/p/commits.html
 * @author Michael Angstadt
 */
class GitHubDao {
	/**
	 * Queries the github API.
	 * @var GitHubApi
	 */
	private $api;
	
	/**
	 * Caches the data from the github API.
	 * @var GitHubCache
	 */
	private $cache;
	
	/**
	 * @param GitHubApi $api queries the github API
	 * @param GitHubCache $cache (optional) caches the data from the github API
	 */
	public function __construct(GitHubApi $api, GitHubCache $cache = null) {
		$this->api = $api;
		$this->cache = $cache;
	}
	/**
	 * Gets the time of the last commit from a GitHub project.
	 * @param string $username the username of the project owner
	 * @param string $projectName the name of the project
	 * @return DateTime the time of the latest commit
	 * @throws Exception if there's any sort of problem making the request
	 */
	public function getLatestCommitTime($username, $projectName){
		$commit = null;
		
		if ($this->cache == null){
			$commit = $this->api->getLatestCommitTime($username, $projectName);
		} else {
			$commitCache = $this->cache->getLatestCommitTime($username, $projectName);
			if ($commitCache == null || $this->cache->needsUpdating($commitCache)){
				try{
					$commit = $this->api->getLatestCommitTime($username, $projectName);
					$this->cache->setLatestCommitTime($username, $projectName, $commit);
				} catch (Exception $e){
					//use the old cached value if there's an error contacting GitHub
					if ($commitCache == null){
						throw $e;
					}
					$commit = $commitCache[0];
					error_log($e->getMessage());
				}
			} else {
				$commit = $commitCache[0];
			}
		}
		
		return $commit;
	}
}