Badoo Jira API Client: magic in Jira in PHP

If you enter “Jira Badoo” in the search string on Habré, the results will take more than one page: we mention it almost everywhere, because it plays an important role in our processes. And each of us wants a little different from her.







The developer, who received the task for the review, expects that a branch is indicated in the task, there are links to diff and the change log. The developer who wrote the code expects to see comments in Jira following the review. The tester, who receives the task after them, wants to see the test results and be able to run the necessary assemblies without going to other interfaces. Product managers generally want to create ten development tasks at the same time by pressing a single button.



And all this is available today and happens automatically. We implemented most of the magic in PHP using the constantly evolving Jira API and using its webhook . And today we want to share with the community our version of the client for this API.



At first, we just wanted to talk about the ideas and approach that we use, and then decided that there was definitely not enough code for such an article for clarity. So there was an open-source version of Badoo Jira PHP Client . Many thanks to ShaggyRatte for helping with her description. And welcome to kat!



More details and context.
If you need more details about what we do with Jira, you can find them in our other articles:



habr.com/en/company/badoo/blog/169417

habr.com/en/company/badoo/blog/433540

habr.com/en/company/badoo/blog/424655



What can he do?



In essence, Badoo Jira PHP Client is a set of ready-made wrapper classes for Jira API responses, most of which can behave like ActiveRecord: they know how to get data about themselves and how to update them on the server, support lazy initialization and data caching at the level code. All Jira entities that you have to work with constantly are wrapped: Issue, Status, Priority, Changelog, User, Version, Component , etc. (almost everything you see in the interface).



In addition, Badoo Jira PHP Client provides a single class hierarchy for all Jira custom fields and is able to generate class definitions for each custom field that you need.



$Issue = new \Badoo\Jira\Issue('SMPL-1'); $Issue->addComment('Sample comment text'); $Issue->attachFile('kitten.jpeg', 'pretty!', 'image/jpeg'); if ($Issue->getStatus()->getName() === 'Open') { $Issue->step('In Progress'); }
      
      





 $DeveloperField = new \Example\CustomFields\Developer($Issue); $DeveloperField->setValue('username')->save();
      
      





 $User = \Badoo\Jira\User::get('username'); $User->watchIssue('SMPL-1'); $User->assign('SMPL-2');
      
      





Thanks to this, interaction with the API from PHP becomes simpler and more convenient, and the documentation for your Jira moves directly to the code, which allows you to use auto-completion in the IDE for many standard actions.



But first things first.



API features we encountered



When we started to actively use the Jira API, it was available only through the SOAP protocol. Its REST version appeared later, and we were among its first users. At that time, there were very few publicly available REST clients written in PHP. It was even harder to find something that could be easily integrated into our code base, gradually moving from SOAP to REST. So we had no choice: we decided to continue developing our own client.



And so we lived, dragging all sorts of hacks and crutches from the SOAP client and acquiring new ones due to the peculiarities of REST. As a result, we have grown some very bold classes with a bunch of duplicated code, and there is a need to clear this mess.



Custom fields have always been the most painful place for us: we have more than 300 of them (at the time of writing this article - 338), and this number is slowly growing.



Strange error messages



Over the long history of interacting with the API, we have seen a lot of different things. Most error messages are adequate, but there are those for which you have to strain your brain a lot.



For example, if Jira suddenly recognizes a robot in your user, she will start showing him a captcha. In this case, when accessing the API, it shamelessly ignores the Accept-Encoding: application / json header and gives you HTML. Naturally, the client that is waiting for JSON may not be ready for this “hello”.



And here is an example of working with a custom field:







When you write code and “test right now” you are testing it, it’s very easy to understand that customfield_12664 is Developers . And if such an error creeps out somewhere on production (for example, because someone changed the configuration and changed the list of acceptable values ​​for the Select field), often the only way to identify the field is to get into Jira and find out the name from there. Moreover, in its interface, IDs are not displayed - only names.



It turns out that when you want to find out the name of the field that led to the error and correct its configuration, you can do this either through a request to the API, or using some other non-obvious methods: rummaging through the source code of the page, opening the settings for an arbitrary field and correcting the URL in the address bar, etc. This process cannot be called convenient, and each time it takes too much time for such a simple task.



But obscure field names are not limited to problems. Here's what the interaction with the API looks like if you make a mistake in the data structure for updating the field:









Many different data formats



And do not forget that updating fields of different types requires different data structures.



 $Jira->issue()->edit( 'SMPL-1', [ 'customfield_10200' => ['name' => 'denkoren'], 'customfield_10300' => ['value' => 'PHP'], 'customfield_10400' => [['value' => 'Android'], ['value' => 'iOS']], 'security' => ['id' => 100500], 'description' => 'Just text', ], );
      
      





The API responses for them are, of course, also different.



Keeping this in mind is only possible if you are constantly working with the Jira API and are not distracted for a long time by solving other problems. Otherwise, these features fly out of memory in a couple of weeks. In addition, you need to remember the type of field you need in order to “feed” it with the correct structure. When you have hundreds of custom fields, you often have to either look in the code where it was still used, or climb into the Jira admin panel.



Before we wrote our client, Stack Overflow and Atlassian Community were my best friends in matters of updating custom fields. Now this information is googled easily and quickly, but we switched to the REST API when it was still fairly new and actively developed: it took ten minutes to find a suitable cURL request in Google, and then you had to parse this bunch of brackets with your eyes and convert into the correct structure for PHP, which often didn’t work on the first try.



In general, interaction with custom fields is the process whose reorganization was required first of all.



What does the client consist of?



Classes for working with custom fields



First of all, we wanted to get rid of remembering data structures for interacting with the API and get readable field names in case of errors.



As a result, we created a single class hierarchy for all custom fields. It turned out three layers:



  1. A common abstract parent for everyone: \ Badoo \ Jira \ CustomFields \ CustomField .
  2. By an abstract class for each type of field: SelectField, UserField, TextField , etc.
  3. By class for each specific field: for example, Developer or Reviewer .


These classes can be written independently, or can be created automatically using a special script-generator (we will come back to it).





Due to this structure, in order to teach the code to update the value of your custom field of type Select List (multiple choice) , it is enough to create a PHP class inherited from SelectField . In fact, every custom Jira field turns into a regular ActiveRecord in PHP code.



 namespace \Example\CustomFields; class Developer extends \Badoo\Jira\CustomFields\SingleUserField { const ID = 'customfield_10200'; const NAME = 'Developer'; } // ,  ,  !
      
      







In the same class, we store information about the field: by default, this is an ID, the name of the field, and a list of available values, if it is limited (for example, for Checkbox and Select ).



Examples of fields in the Jira interface and its corresponding class




 class IssueFor extends \Badoo\Jira\CustomFields\SingleSelectField { const ID = 'customfield_10662'; const NAME = 'Issue for'; /* Available field values. */ const VALUE_BI = 'BI'; const VALUE_C_C = 'C\C++'; const VALUE_HTML_CSS = 'HTML\CSS'; const VALUE_JS = 'JS'; const VALUE_OTHER = 'Other'; const VALUE_PHP = 'PHP'; const VALUE_TRANSLATION = 'Translation'; const VALUES = [ self::VALUE_BI, self::VALUE_C_C, self::VALUE_HTML_CSS, self::VALUE_JS, self::VALUE_OTHER, self::VALUE_PHP, self::VALUE_TRANSLATION, ]; public function getItemsList() : array { return static::VALUES; } }
      
      







It turns out that this kind of documentation is for your Jira, located right in the PHP code. When it is so close, it is very convenient and significantly speeds up development, while reducing the number of errors.



In addition, error messages become more clear: instead of saying nothing, 'customfield_12664' crashes, for example, something like this:



 Uncaught Badoo\Jira\Exception\CustomField: User 'asdkljfh' not found in Jira. Can't change 'Developer' field value.
      
      





Classes for working with system objects



Jira has a lot of data with a complex structure: for example, the Status and Security system fields, links between tasks, users, versions, attachments (files).



We also wrapped them in classes:



 //   $Status = $Issue->getStatus(); $Status->getName(); $Status->getId();
      
      





 // changelog  $History = \Badoo\Jira\Issue\History::forIssue('SMPL-1'); $seconds_in_status = $History->getTimeInStatus('Open');
      
      





 //  Jira $User = new \Badoo\Jira\User('sampleuser'); $User->assign('SMPL-1');
      
      





Such wrappers give your IDE the ability to tell which data is available and allow you to strictly formalize the function interfaces in your code. We actively use type declarations, almost always it allows us to see an error even while writing code thanks to the highlighting of the IDE. And if you still missed the error, it will come out exactly in the place where it first appeared, and not where you finally dropped your code.



There are still static methods that allow you to quickly get an object by some criterion:



 $users = \Badoo\Jira\User::search('<pattern>'); //    login, email  display name $Version = \Badoo\Jira\Version::byName('<project>', '<version name>'); //        $components = \Badoo\Jira\Component::forProject('<project>'); //     
      
      





These methods obey the general rules so that they are easy to find:



Class \ Badoo \ Jira \ Issue



It seems to me that the following screenshot of PhpStorm is quite eloquent in itself:







In essence, the \ Badoo \ Jira \ Issue object binds everything described above into a single system. It stores all the information about the task, it has methods for quick access to the most frequently used data, transfer tasks between statuses, etc.



To create an object in the simplest case, it is enough to know only the key of the task.



Create an object with only a task key in your pocket
 $Issue = new \Badoo\Jira\Issue('SMPL-1');
      
      







You can also use any fragmented dataset. For example, the link information between tasks arriving from the API contains only a few fields: id, summary, status, priority and issuetype. \ Badoo \ Jira \ Issue allows you to collect an object from this data so that it can be returned immediately, and for the rest, access the API.



Create an object, caching values ​​for some fields
  $IssueFromLink = \Badoo\Jira\Issue::fromStdClass( $LinkInfo, [ 'id', 'key', 'summary', 'status', 'priority', 'issuetype', ] );
      
      







This is achieved through lazy initialization and caching of data in the code. This approach is especially convenient in that you can only exchange \ Badoo \ Jira \ Issue objects in your code, regardless of which set of fields they were created with.



Get the missing task data
 $IssueFromLink->getSummary(); //    API,    $IssueFromLink->getDescription(); //  API    description
      
      







How we go to the API
In the Jira API, it is possible to get not all fields for a task, but only the fields that are currently needed: for example, only key and summary. However, we intentionally do not go to Jira for just one field in the getter. In the example above, getDescription () will update information about all fields at once. Since \ Badoo \ Jira \ Issue does not have the slightest idea of ​​what else you need next, it’s more profitable to get everything from the API right away, since we still went there. Yes, the query “get only description” and the query “get all fields by default” for a couple of hundreds of tickets takes a different time, but for one this difference is not so noticeable.



 //Time for single field: 0.40271635055542 (second) //Time for all default fields: 0.84159119129181 (second)
      
      





From the figures it is clear that when receiving only three fields (one in the request), it is more profitable to get everything at once, rather than going to the API for each. The result of this measurement, in fact, depends on the configuration of Jira and the server on which it runs. From task to task and from measurement to measurement, the numbers change and Time for all default fields turns out to be stably less than three Time for single field , and often even less than two.



However, when working with a large number of tasks, the difference can be measured in seconds. Therefore, when you know that you only need key and description for 500 tickets, the ability to get them with one effective query remains in the \ Badoo \ Jira \ Issue :: search () and \ Badoo \ Jira \ Issue :: byKeys () methods .





\ Badoo \ Jira \ Issue - generally about tasks in some abstract Jira. But your (like ours) Jira is not abstract - it has a very specific set of custom fields and its own workflow. You use some of the fields and transitions damn often, so it’s not very convenient to go after them every long way. Therefore, \ Badoo \ Jira \ Issue can be easily extended with its own methods specific to a specific Jira configuration.



An example of a class extension by a method for quickly obtaining a custom field
 namespace \Deploy; class Issue extends \Badoo\Jira\Issue { // … /** * Get 'Developer' custom field as object */ function getDeveloperField() : \Deploy\CustomFields\Developer { return $this->getCustomField(\Deploy\CustomFields\Developer::class); } // ... }
      
      









Issue create request



Creating a task in Jira is a rather complicated procedure. When you do this through the web interface, you are shown a special screen (Create Screen) with a set of fields. You can fill in some of them simply because you want, and some of them are marked as mandatory. At the same time, Create Screen can be unique for each project and even for different types of tasks in one project. So there are all kinds of restrictions on the values ​​of the fields and on the very ability to set the value of the field in the process of creating the task.



The most unpleasant thing for developers in this situation is that these restrictions apply to the API. The latter has a special request ( create-meta has been available in the REST API since version 5.0), with which you can get a list of field settings available when creating a task. However, a developer who needs to "just do a simple thing right now" will most likely not bother with this.



As a result, it happened like this: since the request to create a task can be quite large, we often added data to it gradually, and we received an error when we tried to send everything to Jira. After that, I had to look in the code for all the places where something changed in the request, and for a long and tedious attempt to understand what exactly went wrong.



Therefore, we did \ Badoo \ Jira \ Issue \ CreateRequest . It allows you to see the error earlier, right in the place where you are trying to do something wrong: give the field some kind of curved value or change the field that is not available. For example, if you try to specify a component that does not exist in the project, the exception will crash in the place where you did it, and not where you ultimately sent the request to the API.



The flow of work with CreateRequest looks something like this
 $Request = new \Badoo\Jira\Issue\CreateRequest('DD', 'Task', $Client); $Request ->setSummary('summary') ->setDescription('description') ->setFieldValue('For QA', 'custom field with some comments for QA who will check the issue'); $Request->send();
      
      







Work with API directly



The set of classes described above covers most needs. However, we are well aware that the majority is far from everything. Therefore, we also have a small client for working with the API directly - \ Badoo \ Jira \ REST \ Client .



Customer Use Case
 $Jira = \Badoo\Jira\REST\Client::instance(); $Jira->setJiraUrl('https://jira.example.com/'); $Jira->setAuth('user', 'password') $IssueInfo = $Jira->issue()->get('SMPL-1');
      
      







Class generator for custom fields



To make working with custom fields convenient, each field must have its own class in the code. At our place, we created them manually as needed, but before publishing the client we decided that this approach might not be very convenient for new users. Therefore, we made a special generator that can go to the Jira API for a list of custom fields and create template classes for the known field types.



We believe that for most tasks it is enough to use the bin / generate CLI script from our repository. You can ask him to tell about himself through the --help / -h option:



 ./bin/generate --help
      
      





In the simplest case, for generation it is enough to specify the URL of your Jira, the user, his password, namespace for the classes and the directory where to put the code:



 ./bin/generate -u user -p password --jira-url https://jira.mlan --target-dir custom-fields --namespace CustomFields
      
      





We also implemented the ability to add our own templates and generate classes for individual fields. This can be found in the documentation .



Conclusion



We like what we did. With this concept - our own classes for custom fields, wrappers for statuses, versions, users, etc. - we have been living for more than a year and feel great. Before publishing the code, we even expanded the functionality and added wonderful things that did not reach your hands for a long time to use the client was even more convenient: for example, we added the ability to update several fields in Issue in one request and wrote a class generator for custom fields.



In our opinion, it turned out to be a good thing, which definitely should be felt to understand whether it suits your tasks and requirements. Under ours - just fit.



Link again: github.com/badoo/jira-client .



Thank you for reading to the end. We hope this code will now benefit and save time not only for us.



All Articles