diff --git a/README.md b/README.md index 1754676..f7523df 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ This prevents issues that arise for systems that do not understand the array-ind Use this service to interact with the Harvest API in Greenhouse. Documentation for the Harvest API [can be found here.](https://developers.greenhouse.io/harvest.html/) The purpose of this service is to make interactions with the Harvest API easier. To create a Harvest Service object, you must supply an active Harvest API key. Note that these are different than Job Board API keys. ``` getHarvestService(); + $harvestService = $greenhouseService->getHarvestService(); ?> ``` @@ -186,6 +186,19 @@ If the ID key is supplied in any way, that will take precedence. Ex: [Adding a candidate to Greenhouse](https://developers.greenhouse.io/harvest.html#post-add-candidate) +**A note on paging**: As mentioned in the Harvest documentation, Greenhouse supports two methods of paging depending on the endpoint. The next page is returned in a Link header. The next page link is accessible in the Harvest Service at: + +``` + $harvestService->nextLink(); +``` + +The link returned by this method will give you the next page of objects on this endpoint. Depending on which paging method the endpoint supports, the link returned will look like one of the following: + +- `https://harvest.greenhouse.io/v1/?page=2&per_page=100` +- `https://harvest.greenhouse.io/v1//?since_id=161963` + +If the nextLink() method returns nothing, you have reached the last page. + Greenhouse includes several methods in Harvest to POST new objects. It should be noted that the creation of candidates and applications in Harvest differs from the Application service above. Documents via Harvest can only be received via binary content or by including a URL which contains the document. As such, the Harvest service uses the `body` parameter in Guzzle instead of including POST parameters. ``` $candidate = array( diff --git a/src/Clients/ApiClientInterface.php b/src/Clients/ApiClientInterface.php index 0708be1..7c49a54 100644 --- a/src/Clients/ApiClientInterface.php +++ b/src/Clients/ApiClientInterface.php @@ -47,4 +47,19 @@ public function formatPostParameters(Array $postParameters); * future client. */ public function send($method, $url, Array $options); + + /** + * These methods are to return the paging links as described in the Harvest docs. We return a Link header + * with paging information in next/previous/last format. For Harvest's two paging systems, only the + * getNextLink() method is relevant for both. + */ + public function getNextLink(); + public function getPrevLink(); + public function getLastLink(); + + /** + * Return the raw response from the client. In case users want information that is otherwise unavailable + * through this package. + */ + public function getResponse(); } \ No newline at end of file diff --git a/src/Clients/GuzzleClient.php b/src/Clients/GuzzleClient.php index 0b7bd96..10396af 100644 --- a/src/Clients/GuzzleClient.php +++ b/src/Clients/GuzzleClient.php @@ -5,6 +5,7 @@ use Greenhouse\GreenhouseToolsPhp\Clients\ApiClientInterface; use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Psr7; use Greenhouse\GreenhouseToolsPhp\Clients\Exceptions\GreenhouseAPIClientException; use Greenhouse\GreenhouseToolsPhp\Clients\Exceptions\GreenhouseAPIResponseException; @@ -13,7 +14,11 @@ */ class GuzzleClient implements ApiClientInterface { + public $guzzleResponse; private $_client; + private $_nextLink; + private $_prevLink; + private $_lastLink; /** * Constructor should receive an array that would be understood by the Guzzle @@ -39,7 +44,8 @@ public function __construct($options) public function get($url="") { try { - $guzzleResponse = $this->_client->request('GET', $url); + $this->guzzleResponse = $this->_client->request('GET', $url); + $this->_setLinks(); } catch (RequestException $e) { throw new GreenhouseAPIResponseException($e->getMessage(), 0, $e); } @@ -48,7 +54,7 @@ public function get($url="") * Just return the response cast as a string. The rest of the universe need * not be aware of Guzzle's details. */ - return (string) $guzzleResponse->getBody(); + return (string) $this->guzzleResponse->getBody(); } /** @@ -63,7 +69,7 @@ public function get($url="") public function post(Array $postVars, Array $headers, $url=null) { try { - $guzzleResponse = $this->_client->request( + $this->guzzleResponse = $this->_client->request( 'POST', $url, array('multipart' => $postVars, 'headers' => $headers) @@ -72,18 +78,19 @@ public function post(Array $postVars, Array $headers, $url=null) throw new GreenhouseAPIResponseException($e->getMessage(), 0, $e); } - return (string) $guzzleResponse->getBody(); + return (string) $this->guzzleResponse->getBody(); } public function send($method, $url, Array $options=array()) { try { - $guzzleResponse = $this->_client->request($method, $url, $options); + $this->guzzleResponse = $this->_client->request($method, $url, $options); + $this->_setLinks(); } catch (RequestException $e) { throw new GreenhouseAPIResponseException($e->getMessage(), 0, $e); } - return (string) $guzzleResponse->getBody(); + return (string) $this->guzzleResponse->getBody(); } /** @@ -122,4 +129,41 @@ public function getClient() { return $this->_client; } + + /** + * Set the next/prev/last links using the current response object. + */ + private function _setLinks() + { + $links = Psr7\parse_header($this->guzzleResponse->getHeader('Link')); + foreach ($links as $link) { + if ($link['rel'] == 'last') { + $this->_lastLink = str_replace(['<', '>'], '', $link[0]); + } elseif ($link['rel'] == 'next') { + $this->_nextLink = str_replace(['<', '>'], '', $link[0]); + } elseif ($link['rel'] == 'prev') { + $this->_prevLink = str_replace(['<', '>'], '', $link[0]); + } + } + } + + public function getNextLink() + { + return $this->_nextLink; + } + + public function getPrevLink() + { + return $this->_prevLink; + } + + public function getLastLink() + { + return $this->_lastLink; + } + + public function getResponse() + { + return $this->guzzleResponse; + } } \ No newline at end of file diff --git a/src/Services/HarvestService.php b/src/Services/HarvestService.php index ab6e0a7..d44627a 100644 --- a/src/Services/HarvestService.php +++ b/src/Services/HarvestService.php @@ -42,6 +42,25 @@ public function sendRequest() return $this->_apiClient->send($this->_harvest['method'], $requestUrl, $options); } + /** + * The following 3 methods are for Harvest Paging. They return paging info from the Link header (if it + * exists). Paging is complete if 'nextLink' returns nothing. + */ + public function nextLink() + { + return $this->_apiClient->getNextLink(); + } + + public function prevLink() + { + return $this->_apiClient->getPrevLink(); + } + + public function lastLink() + { + return $this->_apiClient->getLastLink(); + } + /** * In order to keep up to date with changes to the Harvest api and not trigger a re-release of this * package each time a new method is created, the magic Call method is used to construct URLs to the diff --git a/tests/Clients/GuzzleClientTest.php b/tests/Clients/GuzzleClientTest.php index 18042be..e4b305f 100644 --- a/tests/Clients/GuzzleClientTest.php +++ b/tests/Clients/GuzzleClientTest.php @@ -3,6 +3,7 @@ namespace Greenhouse\GreenhouseToolsPhp\Tests\Clients; use Greenhouse\GreenhouseToolsPhp\Clients\GuzzleClient; +use Greenhouse\GreenhouseToolsPhp\Tests\Clients\Mocks\MockGuzzleResponse; class GuzzleClientTest extends \PHPUnit_Framework_TestCase { @@ -72,4 +73,83 @@ public function testFormatPostParametersWithFiles() $this->assertEquals($response[6]['name'], 'resume'); $this->assertEquals($response[6]['filename'], 'resume'); } + + public function testLinksAllIncluded() + { + $linksResponse = array( + '; rel="next",' . + '; rel="prev",' . + '; rel="last"' + ); + + $mockResponse = $this->createMock('Greenhouse\GreenhouseToolsPhp\Tests\Clients\Mocks\MockGuzzleResponse'); + $mockResponse->method('getHeader') + ->willReturn($linksResponse); + $this->client->guzzleResponse = $mockResponse; + + $reflector = new \ReflectionClass('Greenhouse\GreenhouseToolsPhp\Clients\GuzzleClient'); + $method = $reflector->getMethod('_setLinks'); + $method->setAccessible(true); + + $this->assertEquals($this->client->getNextLink(), ''); + $this->assertEquals($this->client->getPrevLink(), ''); + $this->assertEquals($this->client->getLastLink(), ''); + + $method->invokeArgs($this->client, array()); + + $this->assertEquals($this->client->getNextLink(), 'https://harvest.greenhouse.io/v1/candidates?page=3&per_page=100'); + $this->assertEquals($this->client->getPrevLink(), 'https://harvest.greenhouse.io/v1/candidates?page=1&per_page=100'); + $this->assertEquals($this->client->getLastLink(), 'https://harvest.greenhouse.io/v1/candidates?page=8273&per_page=100'); + } + + public function testLinksNoneIncluded() + { + $linksResponse = array(''); + + $mockResponse = $this->createMock('Greenhouse\GreenhouseToolsPhp\Tests\Clients\Mocks\MockGuzzleResponse'); + $mockResponse->method('getHeader') + ->willReturn($linksResponse); + $this->client->guzzleResponse = $mockResponse; + + $reflector = new \ReflectionClass('Greenhouse\GreenhouseToolsPhp\Clients\GuzzleClient'); + $method = $reflector->getMethod('_setLinks'); + $method->setAccessible(true); + + $this->assertEquals($this->client->getNextLink(), ''); + $this->assertEquals($this->client->getPrevLink(), ''); + $this->assertEquals($this->client->getLastLink(), ''); + + $method->invokeArgs($this->client, array()); + + $this->assertEquals($this->client->getNextLink(), ''); + $this->assertEquals($this->client->getPrevLink(), ''); + $this->assertEquals($this->client->getLastLink(), ''); + } + + public function testLinksSomeIncluded() + { + $linksResponse = array( + '; rel="prev",' . + '; rel="last"' + ); + + $mockResponse = $this->createMock('Greenhouse\GreenhouseToolsPhp\Tests\Clients\Mocks\MockGuzzleResponse'); + $mockResponse->method('getHeader') + ->willReturn($linksResponse); + $this->client->guzzleResponse = $mockResponse; + + $reflector = new \ReflectionClass('Greenhouse\GreenhouseToolsPhp\Clients\GuzzleClient'); + $method = $reflector->getMethod('_setLinks'); + $method->setAccessible(true); + + $this->assertEquals($this->client->getNextLink(), ''); + $this->assertEquals($this->client->getPrevLink(), ''); + $this->assertEquals($this->client->getLastLink(), ''); + + $method->invokeArgs($this->client, array()); + + $this->assertEquals($this->client->getNextLink(), ''); + $this->assertEquals($this->client->getPrevLink(), 'https://harvest.greenhouse.io/v1/candidates?page=1&per_page=100'); + $this->assertEquals($this->client->getLastLink(), 'https://harvest.greenhouse.io/v1/candidates?page=8273&per_page=100'); + } } \ No newline at end of file diff --git a/tests/Clients/Mocks/MockGuzzleResponse.php b/tests/Clients/Mocks/MockGuzzleResponse.php new file mode 100644 index 0000000..6f1ae19 --- /dev/null +++ b/tests/Clients/Mocks/MockGuzzleResponse.php @@ -0,0 +1,17 @@ +headers; + } +} \ No newline at end of file diff --git a/tests/Services/HarvestServiceTest.php b/tests/Services/HarvestServiceTest.php index 8479bf0..4e2fd49 100644 --- a/tests/Services/HarvestServiceTest.php +++ b/tests/Services/HarvestServiceTest.php @@ -3,6 +3,7 @@ namespace Greenhouse\GreenhouseToolsPhp\Tests\Services; use Greenhouse\GreenhouseToolsPhp\Services\HarvestService; +use Greenhouse\GreenhouseToolsPhp\GreenhouseService; /** * This test only tests that the service requests generate the expected links and arrays. This does not @@ -15,12 +16,31 @@ public function setUp() { $this->harvestService = new HarvestService('greenhouse'); $apiStub = $this->getMockBuilder('\Greenhouse\GreenhouseToolsPhp\Client\GuzzleClient') - ->setMethods(array('send')) + ->setMethods(array('send', 'getNextLink', 'getPrevLink', 'getLastLink')) ->getMock(); + $apiStub->method('getNextLink')->willReturn('http://example.com/next'); + $apiStub->method('getPrevLink')->willReturn('http://example.com/prev'); + $apiStub->method('getLastLink')->willReturn('http://example.com/last'); + $this->harvestService->setClient($apiStub); $this->expectedAuth = 'Basic Z3JlZW5ob3VzZTo='; } + public function testGetNextLink() + { + $this->assertEquals($this->harvestService->nextLink(), 'http://example.com/next'); + } + + public function testGetPrevLink() + { + $this->assertEquals($this->harvestService->prevLink(), 'http://example.com/prev'); + } + + public function testGetLastLink() + { + $this->assertEquals($this->harvestService->lastLink(), 'http://example.com/last'); + } + public function testGetActivityFeed() { $expected = array(