mekras / jira-client
Jira REST API client with comfortable wrappers for most commonly used API instances like issues, custom fields, components and so on.
Requires
- php: ^7.2
- ext-curl: *
- ext-json: *
- league/climate: ^3.5
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/log: ^1.0
- psr/simple-cache: ^1.0
- symfony/yaml: ^4.2|^5.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^2.15
- nyholm/psr7: ^1.2
- php-http/multipart-stream-builder: ^1.1
- phpunit/phpunit: ^8.2.5
- psr/log: ^1.1
Suggests
- php-http/multipart-stream-builder: Required for using PsrHttpClient
README
PSR-18 HTTP client support
You can use any PSR-18 compatible HTTP client instead of builtin cURL-based. This can be useful for testing.
$adapter = new PsrHttpClient( $httpClient, $requestFactory, $streamFactory ); ClientRaw::instance()->setHttpClient($adapter);
PSR-16 cache support
You can use any PSR-16 compatible cache to reduce number of HTTP requests.
/** @var \Psr\SimpleCache\CacheInterface $ache */ ClientRaw::instance()->setCache($cache);
Original README
Introduction
This is Badoo JIRA REST Client. It contains a bunch of wrapper classes for most common API objects: Issues, Components and so on.
This makes code easier to write because of autocompletion your IDE will provide you.
You can also generate lots of classes for custom fields to get the documentation for your own JIRA installation right in PHP code.
Quick start
Install
composer require badoo/jira-client
Initialize the client
$Jira = \Badoo\Jira\REST\Client::instance(); $Jira ->setJiraUrl('https://jira.example.com/') ->setAuth('user', 'token/password');
Create new issue
$Request = new \Badoo\Jira\Issue\CreateRequest('SMPL', 'Task'); $Request ->setSummary('Awesome issue!') ->setDescription('description of issue created by Badoo JIRA PHP client') ->setLabels(['hey', 'it_works!']) ->addComponent('Other'); $Issue = $Request->send(); print_r( [ 'key' => $Issue->getKey(), 'summary' => $Issue->getSummary(), 'description' => $Issue->getDescription(), ] );
Get the issue
$Issue = new \Badoo\Jira\Issue('SMPL-1'); print_r( [ 'key' => $Issue->getKey(), 'summary' => $Issue->getSummary(), 'description' => $Issue->getDescription(), ] );
Update the issue
$Issue ->setSummary('Awesome issue!') ->setDescription('Yor new description for issue') ->edit('customfield_12345', <value for field>); $Issue->save();
Delete the issue
$Issue = new \Badoo\Jira\Issue('SMPL-1'); $Issue->delete();
Documentation
NOTE: all examples in this documentation related to any interaction with JIRA consider you configured 'global' client object.
Read Configure the client section above to know how to do that.
Client and ClientRaw
The client to JIRA API is split into two parts:
The simplest interface to API: \Badoo\Jira\REST\ClientRaw
It can request API and parse responses.
Throws an \Badoo\Jira\REST\Exception
for API errors or parsed response data when everything went OK.
That's all, it has no other complex logic inside: you decide what URI to request, which type of HTTP request to send (GET, POST, etc.) and what parameters to send.
Consider ClientRaw as a smart wrapper for PHP curl.
$RawClient = new \Badoo\Jira\REST\ClientRaw('https://jira.example.com'); $RawClient->setAuth('user', 'token/password'); $fields = $RawClient->get('/field'); print_r($fields);
Structured client \Badoo\Jira\REST\Client
It is split into several sections, one for each prefix of API methods: e.g. /issue, /field, /project and so on. Each section has bindings to the most popular API methods with parameters it accepts.
The idea is free you from remembering URIs and HTTP request types for common actions. It enables your IDE to give you a hint about available API methods and the options you can provide to each.
Some of sections also cache API responses and have special 'synthetic' methods for most common actions. For example,
you can't get info on particular field by its ID using only API. You have also search through the response.
But with \Badoo\Jira\REST\Client
you can do this
$Client = new \Badoo\Jira\REST\Client('https://jira.example.com/'); $Client->setAuth('user', 'password/token'); $FieldInfo = $Client->field()->get('summary'); print_r($FieldInfo);
When you can't find something in structured client, you still can access Raw client inside it to do everything you need:
$Client = \Badoo\Jira\REST\Client::instance(); $response = $Client->getRawClient()->get('/method/you/wat/to/request', [<parameters]);
The structured client also has a 'global' client object. This object can be accessed via instance() static method:
$Client = \Badoo\Jira\REST\Client::instance();
Under the hood is 'new \Badoo\Jira\REST\Client()', but ::instance() will always return you the same object for all calls to method.
Almost all wrapper classes inside \Badoo\Jira
library require a configured API client to work.
It is always received as the last parameter of any static method or constructor of wrapper and it always defaults to
'global' client when the value was not provided.
Once you configured the global client you don't need to give API client to all wrappers you initialize. They will get it by themselves.
\Badoo\Jira\REST\Client::instance() ->setJiraUrl('https://jira.example.com') ->setAuth('user', 'password/token');
NOTE: all following examples in documentation, related to any interaction with JIRA, consider you configured 'global' client object. That is why we don't pass initialized JIRA API Client to all Issue, CustomField and other objects.
The only reason we left the way to provide API client to all wrappers as a parameter is to enable you to interact with several JIRA installations from one piece of code. For example, if you want to work with your staging and production instances at the same time:
$Prod = new \Badoo\Jira\REST\Client('https://jira.example.com/'); $Prod->setAuth('user', 'password/token'); $Staging = new \Badoo\Jira\REST\Client('https://staging.jira.example.com/'); $Staging->setAuth('user', 'password/token'); $ProdIssue = new \Badoo\Jira\Issue('SMPL-1', $Prod); $StagingIssue = new \Badoo\Jira\Issue('SMPL-1', $Staging); // ...
\Badoo\Jira\Issue class
Getting \Badoo\Jira\Issue instances
To get an issue object you can create it providing only an issue key.
$Issue = new \Badoo\Jira\Issue('SMPL-1');
This is equivalent to:
$Client = \Badoo\Jira\REST\Client::instance(); $Issue = new \Badoo\Jira\Issue('SMPL-1', $Client);
If you want, you can instantiate another API client and provide it to \Badoo\Jira\Issue
constructor.
This might be useful when you have several JIRA instances and want to work with them from single piece of code.
Look at Client and ClientRaw section of documentation to see how to configure instance of API client.
Updating the issue
\Badoo\Jira\Issue
object accumulates changes for fields in internal properties. This means, none of changes you did with
your $Issue object will be applied to real JIRA issue until you call ->save(). This allows you to update issue
in compact way, putting several field changes into a single API request. $Issue object will also continue to return old
field values until you send changes to JIRA with ->save().
$Issue = new \Badoo\Jira\Issue('SMPL-1'); $Issue ->setSummary('new summary') ->setDescription('new description') ->edit('customfield_12345', 'new custom field value'); $Issue->getSummary(); // will return old issue summary, not the one you tried to set 3 lines of code ago $Issue->save(); // makes API request to JIRA, updates all 3 fields you planned $Issue->getSummary(); // will return new issue summary, as expected
Checking if we can edit the field
Not all fields can be changed even if you have them displayed in fields list.
This can be caused by project permissions or issue edit screen configuration. To check if current user can update
field through API, use ->isEditable();
$Issue = new \Badoo\Jira\Issue('SMPL-1'); if ($Issue->isEditable('summary')) { // we can edit summary } else { // we can't edit summary }
Initializing \Badoo\Jira\Issue object on partial fields data
You also can create \Badoo\Jira\Issue
object on data that contains only some fields. For example, you store in your DB
some issues info for your own reasons: key, summary, and description. You can create \Badoo\Jira\Issue
object on this
data without breaking the object logic: it still will load data from API when you need it.
// Consider you get this data from your database: $db_data = [ 'key' => 'SMPL-1', 'summary' => 'summary of example issue', 'description' => 'description of example issue', ]; // First, create an \stdClass object to mimic API response: $IssueInfo = new \stdClass(); $IssueInfo->key = $db_data['key']; $IssueInfo->fields = new \stdClass(); $IssueInfo->fields->summary = $db_data['summary']; $IssueInfo->fields->description = $db_data['description']; // Now we can create an issue object. It will store key, summary and description field values in internal cache // When you need some additional data, e.g. creation time or assignee - object will still load it from API on demand. $Issue = \Badoo\Jira\Issue::fromStdClass($IssueInfo, ['key', 'summary', 'description']);
Custom fields
You can generate a custom field with special generator stored in this repositroy.
For more information follow CFGenerator
subdirectory and open README.md file.
You will find both quickstart and detailed documentation on generator there.
In this section we consider you already created a class for regular custom field, available out of the box in JIRA: 'Checkboxes', 'Number Field', 'Radio Buttons', 'Select List (single choice)' and so on.
Let's consider you created custom field class (or classes) inside \Example\CustomField
namespace.
Field value: get, check, set
$MyCustomField = \Example\CustomFields\MyCustomField::forIssue('SMPL-1'); // get field value from JIRA API $field_value = $MyCustomField->getValue(); $field_is_empty = $Value->isEmpty(); // true when field has no value if ($Value->isEditable()) { $MyCustomField->setValue($MyCustomField::VALUE_AWESOME); // consider this is a select field $MyCustomField->save(); // send API request to update field value in JIRA }
Several custom fields on single issue
When you need to work with several custom fields of the same issue, it is a better practice to use single $Issue object for it:
$Issue = new \Badoo\Jira\Issue('SMPL-1'); $MyCustomField1 = new \Example\CustomFields\MyCustomField1($Issue); $MyCustomField2 = new \Example\CustomFields\MyCustomField2($Issue); $MyCustomField1->setValue('value of first field'); $MyCustomField2->setValue('value of second field'); $Issue->save();
Issue changelog
Changelog of issue has the following structure:
- changelog record 1 (issue update event 1)
- changelog item 1 (field 1 changed)
- changelog item 2 (field 2 changed)
- ...
- changelog record 2 (issue update event 2)
- changelog item 1 (field 1 changed)
- ...
There is a special \Badoo\Jira\History
class designed to work with this data.
It uses its own wrappers for each piece of information from changelog:
\Badoo\Jira\Issue\History
\Badoo\Jira\Issue\HistoryRecord[]
\Badoo\Jira\Issue\LogRecordItem[]
Getting issue's history of changes
If you already have an issue object to work with, just use ->getHistory()
method:
$Issue = new \Badoo\Jira\Issue('SMPL-1'); $History = $Issue->getHistory();
When you have none, just create an object using static method:
$History = \Badoo\Jira\Issue\History::forIssue('SMPL-1');
History class has some useful methods to help you solve most common tasks:
- track field changes,
- calculate time in statuses,
- get last change of issue,
- get last change of specific issue field,
- ...and so on
Discover it's methods using your IDE autocompletion, they might be useful!
Other instances of the Badoo JIRA API Client
Most of the wrapper classes, e.g. User, Status, Priority and so on, have ability to transparently load data from API on demand.
As for CustomFields and Issue objects, you have 2 ways of initialization: with static methods (e.g. ::get()
)
and regular constructor:
$User = new \Badoo\Jira\User(<user name>); $User = \Badoo\Jira\User::byEmail(<user email>);
Most of them have shorthand static methods:
$users = \Badoo\Jira\User::search(<pattern>); // looks for all users with login, email or display name similar to pattern $Version = \Badoo\Jira\Version::byName(<project>, <version name>); // looks for version with specific name in project $components = \Badoo\Jira\Component::forProject(<project>); // lists all components available in project
Names of this methods have similar structure. For convenience we decided to follow next convention:
-
::search() static methods are about multi-criteria search of instances. This is applicable, e.g. for
\Badoo\Jira\Issue::search()
where you use complex JQL queries and\Badoo\Jira\User::search()
where JIRA looks through several user attributes trying to find you a user. -
::get() static methods are about getting a single object by its ID with immediate request to API inside method. This allows you to control where exactly you will get the
\Badoo\Jira\REST\Exception
on API errors if you need it. -
::by() static methods provide you with single or multiple objects identified by some single criteria.
Example:
- \Badoo\Jira\User::byEmail() gives you a JIRA User by its email
- \Badoo\Jira\Version::byName() gives you a JIRA Version by its name.
-
::for() static methods look for all items somehow related to Instance.
Example:
\Badoo\Jira\CustomField::forIssue()
gives you a custom field object related to an issue\Badoo\Jira\Version::forProject()
gives you all versions created in specific project
-
::fromStdClass() method is used by all wrapper classes for initialization on data from API. If you got some information from API with specific request using, say,
\Badoo\Jira\REST\ClientRaw
, you still can operate with typed objects instead of raw \stdClass' onesExample:
$PriorityInfo = \Badoo\Jira\REST\ClientRaw::instance()->get('priority/<priority ID>'); $Priority = \Badoo\Jira\Issue\Priority::fromStdClass($PriorityInfo);
The classes who work as active records and know not only how to load data from API, but also how to set it, use
the same behaviour as \Badoo\Jira\Issue
uses: they accumulate changes within object and push them to API only on
->save()
method call.
Advanced topics
Managing API requests
Once $Issue object is created with 'new' instruction - it has only issue key and client inside. It will load data only when you try to get some field for the first time:
$Issue = new \Badoo\Jira\Issue('SMPL-1'); // no request to API here, just an empty object is returned $Issue->getSummary(); // causes request to JIRA API
When $Issue object loads data from API by himself, it does not select the fields to load. This increases API response time and loads lots of data which is not required 'right now' for getting issue's summary, but \Badoo\Jira\Issue has no idea how many additional ->get() calls it will get, so it is better to load all info once, than ask API many times, when you need the summary, then the description, status, priority and so on.
We compared the time it takes the JIRA to load the data and send it to the client (see examples/measure-load-time.php). It may vary from installation to installation, but almost always (as far as we know - always) the 'get all fields' request will be more effective than 3 'get single field' requests and frequently it will be more effective than 2 ones.
Get single field time: 0.42949662208557
Get all fields time: 0.84061505794525
You can make it do the API call immediately after new instance creation by using one of class' static methods:
$Issue = \Badoo\Jira\Issue::byKey('SMPL-1'); // causes request to JIRA API $Issue->getSummary(); // no request here, object already has all the data on issue
The only thing \Badoo\Jira\Issue
manages inside is 'expand'. JIRA API allows you to request various portions of
information for issue, controlled by 'expand' parameter.
E.g. in most cases you don't need rendered HTML code of fields, or issue changelog.
This data will not be loaded by \Badoo\Jira\Issue
by default when you call ->get().
Only default data provided by JIRA API will be loaded.
When you need an issue history, \Badoo\Jira\Issue
object has to request API once again to get it.
It will also provide object with updated fields information and you will get updated summary, description and so on
if they changed since the last call to API.
In most cases, when you work with a single issue, you don't need to bother yourself with this internal logic
of \Badoo\Jira\Issue
class, but understanding is required to manage API requests in an effective way when you
start to work with lots of issues at the same time:
you can choose several ways of Issue objects initialization and this will
have different side effects on API requests amount and effectiveness.
For example, if you know you need only summary and description for lots of issues, you can request only them. This will dramatically reduce the time of API response:
// load only summary and description for the latest 1000 issues in project 'SMPL'. $issues = \Badoo\Jira\Issue::search('project = SMPL ORDER BY issuekey DESC', ['summary', 'description']); foreach($issues as $Issue) { $Issue->getDescription(); // this will not make \Badoo\Jira\Issue to silently request JIRA API in background $Issue->getPriority(); // but this - will. $Issue object has no status information in cache. }
Issue history can be quite hard to load for JIRA. It affects API response time significantly,
especially when you have long changelogs.
This is the thing you also can optimize by telling \Badoo\Jira\Issue
what do you need:
// load latest 100 issues from project 'SMPL' $issues = \Badoo\Jira\Issue::search( 'project = SMPL ORDER BY issuekey DESC', [], [\Badoo\Jira\REST\Section\Issue::EXP_CHANGELOG], 100 ); foreach ($issues as $Issue) { $description = $Issue->getDescription(); // this will not cause API request $status_changes = $Issue->getHistory()->trackField('status'); // this will not cause API request too! }
Unfortunately, you can't use both $fields and $expand parameters at the same time.
This is because of internal logic of \Badoo\Jira\Issue
cache, that will be broken by such combination.
We will fix this issue in the future if it show up itself as problematic.
Managing API requests with custom fields
You can instantiate a custom field object in several ways. As for \Badoo\Jira\Issue
instantiation,
they differ in API requests required for initialization and values update.
$MyCustomField = \Example\CustomFields\MyJIRACustomField::forIssue('SMPL-1'); // The example above is equivalent to: $Issue = \Badoo\Jira\Issue::byKey('SMPL-1', ['key', \Example\CustomFields\MyJIRACustomField::ID]); $MyCustomField = new \Example\CustomFields\MyJIRACustomField($Issue);
In both examples CustomField object we creted has \Badoo\Jira\Issue object under the hood. The difference reveals when you start to work with several custom fields of one issue.
Initialization with static method ::forIssue()
will always create new \Badoo\Jira\Issue object under the hood.
This means that fields:
$MyCustomField1 = \Example\CustomFields\MyFirstCustomField::forIssue('SMPL-1'); $MyCustomField2 = \Example\CustomFields\MySecondCustomField::forIssue('SMPL-1');
will have different \Badoo\Jira\Issue
objects, even though they are refer to the single JIRA issue.
All custom fields use \Badoo\Jira\Issue
as instrument to manage their values:
they load data through it and edit themselves using interface Issue provides.
When you call $CustomField->setValue()
, it actually is simillar to $Issue->edit(<custom field id>, <new field value>);
.
That means you are able to 'stack' several custom field changes in one $Issue object to send updates to API only once, making interaction with API more optimal.
$Issue = \Badoo\Jira\Issue::byKey('SMPL-1'); // causes API request to get all issue fields $MyCustomField1 = new \Example\CustomFields\MyFirstCustomField($Issue); $MyCustomField2 = new \Example\CustomFields\MySecondCustomField($Issue); // other custom fields initialization $MyCustomField1->setValue('new value'); // no API requests here. Field value in JIRA remains the same $MyCustomField2->setValue($MyCustomField2::VALUE_CHANGED); // no API requests here too. // other custom fields changes $Issue->save(); // API request to JIRA with field updates // Now JIRA issue has new field values and one new changelog record. // You can also use $MyCustomField2->save(); - it is the same, // but with $Issue->save(); it is more clear what is happening
Managing API requests with other classes
Other classes, like Status, Priority and User, have special ::get
static method which duplicates a regular constructor
but has effect on requests to API.
$Status = new \Badoo\Jira\Issue\Status(<status ID>); // no request to API here $Status->getName(); // requests API in background. This is where exception will be thrown on errors. // ... $Status = \Badoo\Jira\Issue\Status::get(<status ID>); // request to API here. This is where exception will be thrown on errors.
Extending \Badoo\Jira\Issue
\Badoo\Jira\Issue
is about abstract JIRA instance. It has no idea about custom fields you oftenly use, statuses you
frequently transition to, and so on. It is much more convenient to have your own shortcuts for actions you do often
To do this, we recomment to craete you own Issue class to extend \Badoo\Jira\Issue
functionality with your own methods.
For example, you might want to easily close issue with one call, setting resolution to some special value. Here is the receipt:
namespace Example; class Issue extends \Badoo\Jira\Issue { public function markDone() : Issue { return $this; } } // ... $Issue = new \Example\Issue('SMPL-1'); $Issue->markDone();
You would probably want to extend \Badoo\Jira\Issue\CreateRequest
to return your Issue object instead of
original one:
namespace Example; class CreateRequest extends \Badoo\Jira\Issue\CreateRequest { public function create() : \Badoo\Jira\Issue { $Issue = parent::create(); $IssueInfo = new \stdClass(); $IssueInfo->id = $Issue->getId(); $IssueInfo->key = $Issue->getKey(); return \Example\Issue::fromStdClass($IssueInfo, ['id', 'key']); } }
Methods to use in child class
Here is just a piece of code with examples. They are much more informative than lost of words.
namespace Example; class Issue extends \Badoo\Jira\Issue { public function getSomeDataFromRawApiResponse() { /** @var \stdClass $IssueInfo - contains an issue data obtained from JIRA API, returned from \Badoo\Jira\ClientRaw 'as-is'. */ $IssueInfo = $this->getBaseIssue(); $issue_key = $IssueInfo->key; $issue_id = $IssueInfo->id; $self_link = $IssueInfo->self; $summary = $IssueInfo->fields->summary; // ... } public function getFieldUsingCache() : \stdClass { return $this->getFieldValue('customfield_12345'); // this is equivalent to // $this->getBaseIssue()->fields->customfield_12345; // but will not cause API request when you use partial field inititialization } public function getMyCustomField() : \Example\CustomFields\MyCustomField { return $this->getCustomField(\Example\CustomFields\MyCustomField::class); // this will also not cause API request when you use partial field initialization, but also return you // the same object of \Example\CustomFields\MyCustomField each time you use the method } }
Writing your own custom field base class
All custom fields should be inherited from \Badoo\Jira\Issue\CustomFields\CustomField
class or one of its children
The simplest examples of custom field base classes are \Badoo\Jira\CustomFields\TextField
and
\Badoo\Jira\CustomFields\NumberField
.
There are some additional special methods you should know about:
$this->getOriginalObject()
- gets field value as it is provided by JIRA API.$this->dropCache()
- drops internal object cache, e.g. drops cached field value.
getOriginalObject()
method requests bound Issue object for current field value.
It caches value inside current object, it is safe to call it multiple times in a row.
This will not cause several API requests.
We expect you to always use this method instead of $this->Issue->getFieldValue()
when you write your own
wrapper inherited directly from \Badoo\Jira\Issue\CustomFields\CustomField
.
dropCache()
method is intended to drop all data about field value cached internally in object. If you plan to use
internal properties in your custom class, don't forget to redefine dropCache()
method so
it clears values of your fields.
dropCache()
method is called by bound Issue object once it loads data from API. This is a way to notify all
existing bound custom field objects that field value might have been updated.