Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Minimal supported PHP version changed from 5.2 to 5.3 by [@chobie].
- The `Api::getPriorties` renamed into `Api::getPriorities` (former method kept for BC reasons) by [@josevh].
- Remove trailing slash from endpoint url by [@Procta].
- Added local cache to getResolutions [@jpastoor].

### Removed
...
Expand All @@ -34,6 +35,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Fixed PHP deprecation notice, when creating issue attachments via `CurlClient` on PHP 5.5+ by [@DerMika].
- The `Api::getRoles` call was always retuning an error by [@aik099].
- Attempt to make a `DELETE` API call using `CurlClient` wasn't working by [@aik099].
- Clearing local caches (statuses, priorities, fields and resolutions) on endpoint change [@jpastoor].

## [1.0.0] - 2014-07-27
### Added
Expand Down
84 changes: 59 additions & 25 deletions src/Jira/Api.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,26 +68,33 @@ class Api
protected $options = self::AUTOMAP_FIELDS;

/**
* Fields.
* Client-side cache of fields. List of fields when loaded, null when nothing is fetched yet.
*
* @var array
* @var array|null
*/
protected $fields;

/**
* Priorities.
* Client-side cache of priorities. List of priorities when loaded, null when nothing is fetched yet.
*
* @var array
* @var array|null
*/
protected $priorities;

/**
* Statuses.
* Client-side cache of statuses. List of statuses when loaded, null when nothing is fetched yet.
*
* @var array
* @var array|null
*/
protected $statuses;

/**
* Client-side cache of resolutions. List of resolutions when loaded, null when nothing is fetched yet.
*
* @var array|null
*/
protected $resolutions;

/**
* Create a JIRA API client.
*
Expand Down Expand Up @@ -141,12 +148,26 @@ public function getEndpoint()
*/
public function setEndpoint($url)
{
$this->fields = array();

// Remove trailing slash in the url.
$url = rtrim($url, '/');

$this->endpoint = $url;
if ( $url !== $this->endpoint ) {
$this->endpoint = $url;
$this->clearLocalCaches();
}
}

/**
* Helper method to clear the local caches. Is called when switching endpoints
*
* @return void
*/
protected function clearLocalCaches()
{
$this->fields = null;
$this->priorities = null;
$this->statuses = null;
$this->resolutions = null;
}

/**
Expand All @@ -156,13 +177,14 @@ public function setEndpoint($url)
*/
public function getFields()
{
if ( !count($this->fields) ) {
// Fetch fields when the method is called for the first time.
if ( $this->fields === null ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw this approach in Symfony, but wouldn't isset($this->fields) be more explicit (I've wrote about this in another comment)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to use strict type comparison instead of the more magicky methods like empty() and isset(). Matter of preference I guess. (empty("0") == true or false? :D)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries. Just use it consistently so we don't me different null checking styles in different places.

Yeah, the empty function is hell. I never use it.

$fields = array();
$_fields = $this->api(self::REQUEST_GET, '/rest/api/2/field', array());
$result = $this->api(self::REQUEST_GET, '/rest/api/2/field', array(), true);

/* set hash key as custom field id */
foreach ( $_fields->getResult() as $k => $v ) {
$fields[$v['id']] = $v;
foreach ( $result as $field ) {
$fields[$field['id']] = $field;
}

$this->fields = $fields;
Expand Down Expand Up @@ -456,13 +478,14 @@ public function findVersionByName($project_key, $name)
*/
public function getPriorities()
{
if ( !count($this->priorities) ) {
// Fetch priorities when the method is called for the first time.
if ( $this->priorities === null ) {
$priorities = array();
$result = $this->api(self::REQUEST_GET, '/rest/api/2/priority', array());
$result = $this->api(self::REQUEST_GET, '/rest/api/2/priority', array(), true);

/* set hash key as custom field id */
foreach ( $result->getResult() as $k => $v ) {
$priorities[$v['id']] = $v;
foreach ( $result as $priority ) {
$priorities[$priority['id']] = $priority;
}

$this->priorities = $priorities;
Expand Down Expand Up @@ -491,13 +514,14 @@ public function getPriorties()
*/
public function getStatuses()
{
if ( !count($this->statuses) ) {
// Fetch statuses when the method is called for the first time.
if ( $this->statuses === null ) {
$statuses = array();
$result = $this->api(self::REQUEST_GET, '/rest/api/2/status', array());
$result = $this->api(self::REQUEST_GET, '/rest/api/2/status', array(), true);

/* set hash key as custom field id */
foreach ( $result->getResult() as $k => $v ) {
$statuses[$v['id']] = $v;
foreach ( $result as $status ) {
$statuses[$status['id']] = $status;
}

$this->statuses = $statuses;
Expand Down Expand Up @@ -875,14 +899,24 @@ public function getProjectIssueTypes($project_key)
/**
* Returns a list of all resolutions.
*
* @param string $project_key Project key.
*
* @return array|false
* @return array
* @since 2.0.0
*/
public function getResolutions()
{
return $this->api(self::REQUEST_GET, '/rest/api/2/resolution', array(), true);
// Fetch resolutions when the method is called for the first time.
if ( $this->resolutions === null ) {
$resolutions = array();
$result = $this->api(self::REQUEST_GET, '/rest/api/2/resolution', array(), true);

foreach ( $result as $resolution ) {
$resolutions[$resolution['id']] = $resolution;
}

$this->resolutions = $resolutions;
}

return $this->resolutions;
}

}
108 changes: 108 additions & 0 deletions tests/Jira/ApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,114 @@ public function testFindVersionByName()
);
}

public function testGetResolutions()
Copy link
Member

@aik099 aik099 Aug 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these tests look almost the same. Proposing to create one universal test with a data provider, that would accept these parameters:

  • json filename (e.g. api_resolution.json)
  • API url (e.g. /rest/api/2/resolution)
  • expected result (maybe we can make resolutions/fields/statuses/priorities. have same IDs and then we don't need to change $expected variable at all

{
$response = file_get_contents(__DIR__ . '/resources/api_resolution.json');

$this->expectClientCall(
Api::REQUEST_GET,
'/rest/api/2/resolution',
array(),
$response
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use json_encode($response) here.

);

$actual = $this->api->getResolutions();

$response_decoded = json_decode($response, true);

$expected = array(
'1' => $response_decoded[0],
'10000' => $response_decoded[1],
);
$this->assertEquals($expected, $actual);

// Second time we call the method the results should be cached and not trigger an API Request.
$this->client->sendRequest(Api::REQUEST_GET, '/rest/api/2/resolution', array(), self::ENDPOINT, $this->credential)
Copy link
Member

@aik099 aik099 Aug 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tried to remove caching layer in getResolutions method and checked that test will fail because of this expectation?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep that works

->shouldNotBeCalled();
$this->assertEquals($expected, $this->api->getResolutions(), 'Calling twice did not yield the same results');
}

public function testGetFields()
{
$response = file_get_contents(__DIR__ . '/resources/api_field.json');

$this->expectClientCall(
Api::REQUEST_GET,
'/rest/api/2/field',
array(),
$response
);

$actual = $this->api->getFields();

$response_decoded = json_decode($response, true);

$expected = array(
'issuetype' => $response_decoded[0],
'timespent' => $response_decoded[1],
);
$this->assertEquals($expected, $actual);

// Second time we call the method the results should be cached and not trigger an API Request.
$this->client->sendRequest(Api::REQUEST_GET, '/rest/api/2/field', array(), self::ENDPOINT, $this->credential)
->shouldNotBeCalled();
$this->assertEquals($expected, $this->api->getFields(), 'Calling twice did not yield the same results');
}

public function testGetStatuses()
{
$response = file_get_contents(__DIR__ . '/resources/api_status.json');

$this->expectClientCall(
Api::REQUEST_GET,
'/rest/api/2/status',
array(),
$response
);

$actual = $this->api->getStatuses();

$response_decoded = json_decode($response, true);

$expected = array(
'1' => $response_decoded[0],
'3' => $response_decoded[1],
);
$this->assertEquals($expected, $actual);

// Second time we call the method the results should be cached and not trigger an API Request.
$this->client->sendRequest(Api::REQUEST_GET, '/rest/api/2/status', array(), self::ENDPOINT, $this->credential)
->shouldNotBeCalled();
$this->assertEquals($expected, $this->api->getStatuses(), 'Calling twice did not yield the same results');
}

public function testGetPriorities()
{
$response = file_get_contents(__DIR__ . '/resources/api_priority.json');

$this->expectClientCall(
Api::REQUEST_GET,
'/rest/api/2/priority',
array(),
$response
);

$actual = $this->api->getPriorities();

$response_decoded = json_decode($response, true);

$expected = array(
'1' => $response_decoded[0],
'5' => $response_decoded[1],
);
$this->assertEquals($expected, $actual);

// Second time we call the method the results should be cached and not trigger an API Request.
$this->client->sendRequest(Api::REQUEST_GET, '/rest/api/2/priority', array(), self::ENDPOINT, $this->credential)
->shouldNotBeCalled();
$this->assertEquals($expected, $this->api->getPriorities(), 'Calling twice did not yield the same results');
}

/**
* Expects a particular client call.
*
Expand Down
35 changes: 35 additions & 0 deletions tests/Jira/resources/api_field.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[
{
"id": "issuetype",
"key": "issuetype",
"name": "Issue Type",
"custom": false,
"orderable": true,
"navigable": true,
"searchable": true,
"clauseNames": [
"issuetype",
"type"
],
"schema": {
"type": "issuetype",
"system": "issuetype"
}
},
{
"id": "timespent",
"key": "timespent",
"name": "Time Spent",
"custom": false,
"orderable": false,
"navigable": true,
"searchable": false,
"clauseNames": [
"timespent"
],
"schema": {
"type": "number",
"system": "timespent"
}
}
]
18 changes: 18 additions & 0 deletions tests/Jira/resources/api_priority.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"self": "https://test.atlassian.net/rest/api/2/priority/1",
"statusColor": "#cc0000",
"description": "Blocks development and/or testing work, production could not run.",
"iconUrl": "https://test.atlassian.net/images/icons/priorities/blocker.svg",
"name": "Blocker",
"id": "1"
},
{
"self": "https://test.atlassian.net/rest/api/2/priority/5",
"statusColor": "#003300",
"description": "Cosmetic problem like misspelt words or misaligned text.",
"iconUrl": "https://test.atlassian.net/images/icons/priorities/trivial.svg",
"name": "Trivial",
"id": "5"
}
]
14 changes: 14 additions & 0 deletions tests/Jira/resources/api_resolution.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"self": "https://test.atlassian.net/rest/api/2/resolution/1",
"id": "1",
"description": "A fix for this issue is checked into the tree and tested.",
"name": "Fixed"
},
{
"self": "https://test.atlassian.net/rest/api/2/resolution/10000",
"id": "10000",
"description": "This issue won't be actioned.",
"name": "Won't Do"
}
]
30 changes: 30 additions & 0 deletions tests/Jira/resources/api_status.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[
{
"self": "https://test.atlassian.net/rest/api/2/status/1",
"description": "The issue is open and ready for the assignee to start work on it.",
"iconUrl": "https://test.atlassian.net/images/icons/statuses/open.png",
"name": "Open",
"id": "1",
"statusCategory": {
"self": "https://test.atlassian.net/rest/api/2/statuscategory/2",
"id": 2,
"key": "new",
"colorName": "blue-gray",
"name": "To Do"
}
},
{
"self": "https://test.atlassian.net/rest/api/2/status/3",
"description": "This issue is being actively worked on at the moment by the assignee.",
"iconUrl": "https://test.atlassian.net/images/icons/statuses/inprogress.png",
"name": "In Progress",
"id": "3",
"statusCategory": {
"self": "https://test.atlassian.net/rest/api/2/statuscategory/4",
"id": 4,
"key": "indeterminate",
"colorName": "yellow",
"name": "In Progress"
}
}
]