Skip to content

Commit c1baf7f

Browse files
committed
Use If-modified-since headers to avoid X-Rate-Limit decrease
1 parent 65e9828 commit c1baf7f

File tree

9 files changed

+287
-13
lines changed

9 files changed

+287
-13
lines changed

lib/Github/Client.php

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,11 @@ class Client
6767
/**
6868
* Instantiate a new GitHub client
6969
*
70-
* @param null|ClientInterface $httpClient Buzz client
70+
* @param null|HttpClientInterface $httpClient Github http client
7171
*/
72-
public function __construct(ClientInterface $httpClient = null)
72+
public function __construct(HttpClientInterface $httpClient = null)
7373
{
74-
$httpClient = $httpClient ?: new Curl();
75-
$httpClient->setTimeout($this->options['timeout']);
76-
$httpClient->setVerifyPeer(false);
77-
78-
$this->httpClient = new HttpClient($this->options, $httpClient);
74+
$this->httpClient = $httpClient ?: new HttpClient($this->options);
7975
}
8076

8177
/**
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace Github\HttpClient\Cache;
4+
5+
use Github\HttpClient\Message\Response;
6+
7+
/**
8+
* Caches github api responses
9+
*
10+
* @author Florian Klein <florian.klein@free.fr>
11+
*/
12+
interface CacheInterface
13+
{
14+
/**
15+
* @param string the id of the cached resource
16+
* @return int the modified since timestamp
17+
**/
18+
public function getModifiedSince($id);
19+
20+
/**
21+
* @param string the id of the cached resource
22+
* @return Response The cached response object
23+
**/
24+
public function get($id);
25+
26+
/**
27+
* @param string the id of the cached resource
28+
* @param Response the response to cache
29+
**/
30+
public function set($id, Response $response);
31+
}
32+
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace Github\HttpClient\Cache;
4+
5+
use Github\HttpClient\Message\Response;
6+
7+
class FilesystemCache implements CacheInterface
8+
{
9+
protected $path;
10+
11+
public function __construct($path = null)
12+
{
13+
$this->path = $path ?: sys_get_temp_dir().DIRECTORY_SEPARATOR.'php-github-api-cache';
14+
}
15+
16+
public function get($id)
17+
{
18+
if (false !== $content = @file_get_contents($this->getPath($id))) {
19+
return unserialize($content);
20+
}
21+
22+
throw new \InvalidArgumentException(sprintf('File "%s" not found', $this->getPath($id)));
23+
}
24+
25+
public function set($id, Response $response)
26+
{
27+
if (!is_dir($this->path)) {
28+
mkdir($this->path, 0777, true);
29+
}
30+
31+
if (false === $bytes = @file_put_contents($this->getPath($id), serialize($response))) {
32+
throw new \InvalidArgumentException(sprintf('Cannot put content in file "%s"', $this->getPath($id)));
33+
}
34+
}
35+
36+
public function getModifiedSince($id)
37+
{
38+
if (file_exists($this->getPath($id))) {
39+
return filemtime($this->getPath($id));
40+
}
41+
}
42+
43+
protected function getPath($id)
44+
{
45+
return sprintf('%s%s%s', rtrim($this->path, DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR, md5($id));
46+
}
47+
}
48+
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace Github\HttpClient;
4+
5+
use Buzz\Client\ClientInterface;
6+
use Buzz\Message\MessageInterface;
7+
use Buzz\Message\RequestInterface;
8+
use Buzz\Listener\ListenerInterface;
9+
10+
use Github\Exception\ErrorException;
11+
use Github\Exception\RuntimeException;
12+
use Github\HttpClient\Listener\ErrorListener;
13+
use Github\HttpClient\Message\Request;
14+
use Github\HttpClient\Message\Response;
15+
use Github\HttpClient\Cache\CacheInterface;
16+
use Github\HttpClient\Cache\FilesystemCache;
17+
18+
/**
19+
* Performs requests on GitHub API using If-Modified-Since headers.
20+
* Returns a cached version if not modified
21+
* Avoids increasing the X-Rate-Limit, which is cool
22+
*
23+
* @author Florian Klein <florian.klein@free.fr>
24+
*/
25+
class CachedHttpClient extends HttpClient
26+
{
27+
protected $cache;
28+
29+
public function __construct(array $options = array(), ClientInterface $client = null, CacheInterface $cache = null)
30+
{
31+
parent::__construct($options, $client);
32+
33+
$this->cache = $cache ?: new FilesystemCache;
34+
}
35+
36+
public function request($path, array $parameters = array(), $httpMethod = 'GET', array $headers = array(), Response $response = null)
37+
{
38+
$response = parent::request($path, $parameters, $httpMethod, $headers, $response);
39+
40+
$key = trim($this->options['base_url'].$path, '/');
41+
if ($response->isNotModified()) {
42+
return $this->cache->get($key);
43+
}
44+
45+
$this->cache->set($key, $response);
46+
47+
return $response;
48+
}
49+
50+
/**
51+
* Create requests with If-Modified-Since headers
52+
* @param string $httpMethod
53+
* @param string $url
54+
*
55+
* @return Request
56+
*/
57+
protected function createRequest($httpMethod, $url)
58+
{
59+
$request = parent::createRequest($httpMethod, $url);
60+
$modifiedSince = date('r', $this->cache->getModifiedSince($url));
61+
$request->addHeader(sprintf('If-Modified-Since: %s', $modifiedSince));
62+
63+
return $request;
64+
}
65+
}

lib/Github/HttpClient/HttpClient.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Github\HttpClient\Listener\ErrorListener;
1313
use Github\HttpClient\Message\Request;
1414
use Github\HttpClient\Message\Response;
15+
use Buzz\Client\Curl;
1516

1617
/**
1718
* Performs requests on GitHub API. API documentation should be self-explanatory.
@@ -48,8 +49,12 @@ class HttpClient implements HttpClientInterface
4849
* @param array $options
4950
* @param ClientInterface $client
5051
*/
51-
public function __construct(array $options, ClientInterface $client)
52+
public function __construct(array $options = array(), ClientInterface $client = null)
5253
{
54+
$client = $client ?: new Curl();
55+
$client->setTimeout($this->options['timeout']);
56+
$client->setVerifyPeer(false);
57+
5358
$this->options = array_merge($this->options, $options);
5459
$this->client = $client;
5560

@@ -139,7 +144,7 @@ public function put($path, array $parameters = array(), array $headers = array()
139144
/**
140145
* {@inheritDoc}
141146
*/
142-
public function request($path, array $parameters = array(), $httpMethod = 'GET', array $headers = array())
147+
public function request($path, array $parameters = array(), $httpMethod = 'GET', array $headers = array(), Response $response = null)
143148
{
144149
$path = trim($this->options['base_url'].$path, '/');
145150

@@ -154,7 +159,9 @@ public function request($path, array $parameters = array(), $httpMethod = 'GET',
154159
}
155160
}
156161

157-
$response = new Response();
162+
if (null === $response) {
163+
$response = new Response;
164+
}
158165

159166
try {
160167
$this->client->send($request, $response);
@@ -198,7 +205,7 @@ public function getLastResponse()
198205
*
199206
* @return Request
200207
*/
201-
private function createRequest($httpMethod, $url)
208+
protected function createRequest($httpMethod, $url)
202209
{
203210
$request = new Request($httpMethod);
204211
$request->setHeaders($this->headers);

lib/Github/HttpClient/HttpClientInterface.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Github\HttpClient;
44

55
use Github\Exception\InvalidArgumentException;
6+
use Github\HttpClient\Message\Response;
67

78
/**
89
* Performs requests on GitHub API. API documentation should be self-explanatory.
@@ -77,7 +78,7 @@ public function delete($path, array $parameters = array(), array $headers = arra
7778
*
7879
* @return array Data
7980
*/
80-
public function request($path, array $parameters = array(), $httpMethod = 'GET', array $headers = array());
81+
public function request($path, array $parameters = array(), $httpMethod = 'GET', array $headers = array(), Response $response = null);
8182

8283
/**
8384
* Change an option value.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace Github\Tests\HttpClient\Cache;
4+
5+
use Github\HttpClient\Message\Response;
6+
use Github\HttpClient\Cache\FilesystemCache;
7+
8+
class FilesystemCacheTest extends \PHPUnit_Framework_TestCase
9+
{
10+
/**
11+
* @test
12+
*/
13+
public function shouldStoreAResponseForAGivenKey()
14+
{
15+
$cache = new FilesystemCache('/tmp/github-api-test');
16+
17+
$cache->set('test', new Response);
18+
19+
$this->assertNotNull($cache->get('test'));
20+
}
21+
22+
/**
23+
* @test
24+
*/
25+
public function shouldGetATimestampForExistingFile()
26+
{
27+
$cache = new FilesystemCache('/tmp/github-api-test');
28+
29+
$cache->set('test', new Response);
30+
31+
$this->assertInternalType('int', $cache->getModifiedSince('test'));
32+
}
33+
34+
/**
35+
* @test
36+
*/
37+
public function shouldNotGetATimestampForInexistingFile()
38+
{
39+
$cache = new FilesystemCache('/tmp/github-api-test');
40+
41+
$this->assertNull($cache->getModifiedSince('test2'));
42+
}
43+
}
44+
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
namespace Github\Tests\HttpClient;
4+
5+
use Github\HttpClient\CachedHttpClient;
6+
use Github\HttpClient\Message\Response;
7+
use Github\HttpClient\Message\Request;
8+
9+
class CachedHttpClientTest extends HttpClientTest
10+
{
11+
/**
12+
* @test
13+
*/
14+
public function shouldCacheResponseAtFirstTime()
15+
{
16+
$cache = $this->getCacheMock();
17+
$httpClient = new CachedHttpClient(
18+
array('base_url' => ''),
19+
$this->getMock('Buzz\Client\ClientInterface'),
20+
$cache
21+
);
22+
23+
$cache->expects($this->once())->method('set')->with('test', new Response);
24+
25+
$httpClient->get('test');
26+
}
27+
28+
/**
29+
* @test
30+
*/
31+
public function shouldGetCachedResponseWhileResourceNotModified()
32+
{
33+
$client = $this->getMock('Buzz\Client\ClientInterface');
34+
$client->expects($this->once())->method('send');
35+
36+
$cache = $this->getCacheMock();
37+
38+
$httpClient = new CachedHttpClient(
39+
array('base_url' => ''),
40+
$client,
41+
$cache
42+
);
43+
44+
$cache->expects($this->once())->method('get')->with('test');
45+
46+
$response = new Response;
47+
$response->addHeader('HTTP/1.1 304 Not Modified');
48+
49+
$httpClient->request('test', array(), 'GET', array(), $response);
50+
}
51+
52+
/**
53+
* @test
54+
*/
55+
public function shouldRenewCacheWhenResourceHasChanged()
56+
{
57+
$client = $this->getMock('Buzz\Client\ClientInterface');
58+
$client->expects($this->once())->method('send');
59+
60+
$cache = $this->getCacheMock();
61+
62+
$httpClient = new CachedHttpClient(
63+
array('base_url' => ''),
64+
$client,
65+
$cache
66+
);
67+
68+
$response = new Response;
69+
$response->addHeader('HTTP/1.1 200 OK');
70+
71+
$cache->expects($this->once())->method('set')->with('test', $response);
72+
$cache->expects($this->once())->method('getModifiedSince')->with('test')->will($this->returnValue(1256953732));
73+
74+
$httpClient->request('test', array(), 'GET', array(), $response);
75+
}
76+
77+
public function getCacheMock()
78+
{
79+
return $this->getMock('Github\HttpClient\Cache\CacheInterface');
80+
}
81+
}

test/Github/Tests/HttpClient/HttpClientTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ public function clearHeaders()
215215
{
216216
}
217217

218-
public function request($path, array $parameters = array(), $httpMethod = 'GET', array $headers = array())
218+
public function request($path, array $parameters = array(), $httpMethod = 'GET', array $headers = array(), Response $response = null)
219219
{
220220
$request = new Request($httpMethod);
221221
$response = $this->fakeResponse ? $this->fakeResponse : new Response();

0 commit comments

Comments
 (0)