mekras / jira-client
Jira REST API 客户端,为常见 API 实例(如问题、自定义字段、组件等)提供便捷的包装。
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 客户端支持
您可以使用任何兼容 PSR-18 的 HTTP 客户端代替内置的基于 cURL 的客户端。这对于测试可能很有用。
$adapter = new PsrHttpClient( $httpClient, $requestFactory, $streamFactory ); ClientRaw::instance()->setHttpClient($adapter);
PSR-16 缓存支持
您可以使用任何兼容 PSR-16 的缓存来减少 HTTP 请求的数量。
/** @var \Psr\SimpleCache\CacheInterface $ache */ ClientRaw::instance()->setCache($cache);
原始 README
介绍
这是 Badoo JIRA REST 客户端。它包含了一组用于最常见 API 对象(如问题、组件等)的包装类。
这使代码更容易编写,因为您的 IDE 将提供自动完成功能。
您还可以为自定义字段生成大量类,以在 PHP 代码中获得您自己的 JIRA 安装的文档。
快速开始
安装
composer require badoo/jira-client
初始化客户端
$Jira = \Badoo\Jira\REST\Client::instance(); $Jira ->setJiraUrl('https://jira.example.com/') ->setAuth('user', 'token/password');
创建新问题
$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(), ] );
获取问题
$Issue = new \Badoo\Jira\Issue('SMPL-1'); print_r( [ 'key' => $Issue->getKey(), 'summary' => $Issue->getSummary(), 'description' => $Issue->getDescription(), ] );
更新问题
$Issue ->setSummary('Awesome issue!') ->setDescription('Yor new description for issue') ->edit('customfield_12345', <value for field>); $Issue->save();
删除问题
$Issue = new \Badoo\Jira\Issue('SMPL-1'); $Issue->delete();
文档
注意:本文档中所有与 JIRA 交互的示例都考虑您已配置了 '全局' 客户端对象。
阅读上述 配置客户端 部分,了解如何做到这一点。
客户端和 ClientRaw
JIRA API 的客户端分为两部分
API 的最简单接口:\Badoo\Jira\REST\ClientRaw
它可以请求 API 并解析响应。当一切顺利时,它将抛出 \Badoo\Jira\REST\Exception
,对于 API 错误或解析的响应数据。
仅此而已,它内部没有其他复杂逻辑:您决定请求哪个 URI,发送哪种类型的 HTTP 请求(GET、POST 等)以及发送哪些参数。
将 ClientRaw 视为 PHP curl 的智能包装器。
$RawClient = new \Badoo\Jira\REST\ClientRaw('https://jira.example.com'); $RawClient->setAuth('user', 'token/password'); $fields = $RawClient->get('/field'); print_r($fields);
结构化客户端 \Badoo\Jira\REST\Client
它分为几个部分,每个部分对应 API 方法的每个前缀:例如 /issue、/field、/project 等。每个部分都有绑定到它接受的参数的最流行 API 方法的绑定。
目的是让您免于记住常见操作的 URI 和 HTTP 请求类型。它使您的 IDE 能够提供有关可用 API 方法和您可以为每个提供的选项的提示。
某些部分还缓存 API 响应,并具有针对常见操作的特别 '合成' 方法。例如,您不能仅使用 API 通过 ID 获取特定字段的详细信息。您还必须遍历响应。但使用 \Badoo\Jira\REST\Client
,您可以这样做
$Client = new \Badoo\Jira\REST\Client('https://jira.example.com/'); $Client->setAuth('user', 'password/token'); $FieldInfo = $Client->field()->get('summary'); print_r($FieldInfo);
当您在结构化客户端中找不到任何内容时,您仍然可以访问其内部的原始客户端以完成所有需要的工作
$Client = \Badoo\Jira\REST\Client::instance(); $response = $Client->getRawClient()->get('/method/you/wat/to/request', [<parameters]);
结构化客户端还有一个 '全局' 客户端对象。此对象可以通过 instance() 静态方法访问
$Client = \Badoo\Jira\REST\Client::instance();
在内部是 'new \Badoo\Jira\REST\Client()', 但 ::instance() 将始终为所有方法调用返回相同的对象。
《Badoo\Jira》库中几乎所有包装类都需要配置API客户端才能工作。它始终作为任何静态方法或包装构造函数的最后一个参数接收,如果没有提供值,则默认为'global'客户端。
一旦配置了全局客户端,您就不需要为所有初始化的包装器提供API客户端。它们会自动获取。
\Badoo\Jira\REST\Client::instance() ->setJiraUrl('https://jira.example.com') ->setAuth('user', 'password/token');
注意:文档中所有后续示例,涉及任何与JIRA的交互,都假定您已配置了'global'客户端对象。这就是为什么我们不将初始化的JIRA API客户端传递给所有Issue、CustomField和其他对象。
我们留下将API客户端作为参数传递给所有包装器的方法,唯一原因是为了使您能够从一段代码中与多个JIRA安装交互。例如,如果您想同时处理您的预发布和生成实例
$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 类
获取 \Badoo\Jira\Issue 实例
要获取一个问题对象,您只需要提供一个问题键。
$Issue = new \Badoo\Jira\Issue('SMPL-1');
这相当于
$Client = \Badoo\Jira\REST\Client::instance(); $Issue = new \Badoo\Jira\Issue('SMPL-1', $Client);
如果您愿意,可以实例化另一个API客户端并将其提供给 \Badoo\Jira\Issue
构造函数。当您有多个JIRA实例并希望从一段代码中与它们交互时,这可能很有用。请参阅文档中的客户端和ClientRaw部分,了解如何配置API客户端实例。
更新问题
\Badoo\Jira\Issue
对象会在内部属性中累积字段更改。这意味着,您对$Issue对象所做的任何更改都不会应用于实际的JIRA问题,直到您调用 ->save()。这允许您以紧凑的方式更新问题,将多个字段更改放入单个API请求中。$Issue对象还将继续返回旧字段值,直到您使用 ->save() 将更改发送到JIRA。
$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
检查是否可以编辑字段
即使您在字段列表中显示了所有字段,也不能更改所有字段。这可能是由项目权限或问题编辑屏幕配置引起的。要检查当前用户是否可以通过API更新字段,请使用 ->isEditable();
$Issue = new \Badoo\Jira\Issue('SMPL-1'); if ($Issue->isEditable('summary')) { // we can edit summary } else { // we can't edit summary }
在部分字段数据上初始化 \Badoo\Jira\Issue 对象
您还可以在仅包含一些字段的数据上创建 \Badoo\Jira\Issue
对象。例如,您可能出于自己的原因在数据库中存储一些问题信息:key、摘要和描述。您可以在这些数据上创建 \Badoo\Jira\Issue
对象,而不会破坏对象逻辑:当您需要时,它仍然会从API加载数据。
// 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']);
自定义字段
您可以使用存储在此存储库中的特殊生成器生成自定义字段。有关更多信息,请参阅 CFGenerator
子目录并打开 README.md 文件。您将在其中找到生成器的快速入门和详细文档。
在本节中,我们假设您已创建了一个类,用于在JIRA中开箱即用的常规自定义字段:“复选框”、“数字字段”、“单选按钮”、“选择列表(单选)”等。
假设您在 \Example\CustomField
命名空间内创建了自定义字段类(或类)。
字段值:获取、检查、设置
$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 }
单一个问题上的多个自定义字段
当您需要处理同一问题上的多个自定义字段时,最好使用单个$Issue对象。
$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();
问题变更日志
问题更改日志的结构如下
- 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)
- ...
有一个特殊的 \Badoo\Jira\History
类专门用于处理这些数据。它使用自己的包装器为更改日志中的每条信息提供包装
\Badoo\Jira\Issue\History
\Badoo\Jira\Issue\HistoryRecord[]
\Badoo\Jira\Issue\LogRecordItem[]
获取问题的更改历史
如果您已经有一个要处理的问题对象,只需使用 ->getHistory()
方法。
$Issue = new \Badoo\Jira\Issue('SMPL-1'); $History = $Issue->getHistory();
如果没有,只需使用静态方法创建一个对象。
$History = \Badoo\Jira\Issue\History::forIssue('SMPL-1');
历史类有一些有用的方法,可以帮助您解决大多数常见任务。
- 跟踪字段更改,
- 计算状态中的时间,
- 获取问题的最后更改,
- 获取特定问题的字段最后更改,
- ...等等
使用您的IDE自动完成功能发现它的方法,它们可能很有用!
Badoo JIRA API客户端的其他实例
大多数包装类,例如User、Status、Priority等,都有能力在需要时透明地从API加载数据。
对于CustomFields和Issue对象,您有两种初始化方式:使用静态方法(例如::get()
)和常规构造函数
$User = new \Badoo\Jira\User(<user name>); $User = \Badoo\Jira\User::byEmail(<user email>);
它们大多数都有简写静态方法
$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
这些方法名称具有相似的结构。为了方便起见,我们决定遵循以下约定
-
::search() 静态方法涉及实例的多条件搜索。这适用于例如
\Badoo\Jira\Issue::search()
,其中您使用复杂的JQL查询,以及\Badoo\Jira\User::search()
,其中JIRA通过几个用户属性尝试找到您。 -
::get() 静态方法涉及通过其ID获取单个对象,并在方法内部立即请求API。这允许您控制在需要时您将在哪里得到
\Badoo\Jira\REST\Exception
的API错误。 -
::by() 静态方法提供通过某些单一标准标识的单个或多个对象。
示例
- \Badoo\Jira\User::byEmail() 通过电子邮件为您提供JIRA用户
- \Badoo\Jira\Version::byName() 通过名称为您提供JIRA版本。
-
::for() 静态方法寻找与实例以某种方式相关联的所有项。
示例
\Badoo\Jira\CustomField::forIssue()
给您与问题相关的自定义字段对象\Badoo\Jira\Version::forProject()
给您在特定项目中创建的所有版本
-
::fromStdClass() 方法由所有包装类用于从API数据初始化。如果您使用,例如
\Badoo\Jira\REST\ClientRaw
,从特定请求中获取了一些信息,您仍然可以使用类型对象而不是原始\stdClass'对象来操作示例
$PriorityInfo = \Badoo\Jira\REST\ClientRaw::instance()->get('priority/<priority ID>'); $Priority = \Badoo\Jira\Issue\Priority::fromStdClass($PriorityInfo);
作为活动记录工作的类不仅知道如何从API加载数据,还知道如何设置数据,它们使用与\Badoo\Jira\Issue
相同的行为:它们在对象内部累积更改,并在调用->save()
方法时将其推送到API。
高级主题
管理 API 请求
一旦使用'new'指令创建了$Issue对象 - 它只包含问题密钥和客户端。它将在您尝试第一次获取某些字段时加载数据
$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
当$Issue对象自己从API加载数据时,它不会选择要加载数据的字段。这增加了API响应时间,并加载了大量当前不需要获取问题摘要、但Badoo\Jira\Issue不知道它将得到多少额外的->get()调用的数据。因此,最好一次性加载所有信息,而不是在需要摘要时请求API,然后是描述、状态、优先级等等。
我们比较了JIRA加载数据并发送给客户端所需的时间(请参阅examples/measure-load-time.php)。它可能因安装而异,但几乎总是(据我们所知,始终)'获取所有字段'请求比3个'获取单个字段'请求更有效,并且通常比2个更有效。
Get single field time: 0.42949662208557
Get all fields time: 0.84061505794525
您可以使用类的一个静态方法在创建新实例后立即执行API调用
$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
\Badoo\Jira\Issue
内部管理的唯一事物是'expand'。JIRA API允许您通过'expand'参数请求问题的各种信息部分。例如,在大多数情况下,您不需要字段的渲染HTML代码或问题更改日志。默认情况下,当您调用->get()时,这些数据不会被\Badoo\Jira\Issue
加载。只有JIRA API提供的默认数据将被加载。
当您需要问题历史记录时,\Badoo\Jira\Issue
对象必须再次请求API以获取它。它还将提供具有更新字段信息的对象,如果它们自上次调用API以来已更改,您将获得更新的摘要、描述等。
在大多数情况下,当您处理单个问题时,您不需要关心 \Badoo\Jira\Issue
类的内部逻辑,但了解这一点对于在同时处理大量问题时以有效的方式管理API请求是必要的:您可以选择几种初始化 Issue 对象的方式,这将对API请求的数量和有效性产生不同的影响。
例如,如果您知道您只需要大量问题的摘要和描述,您只需请求它们。这将大大减少API响应时间
// 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. }
JIRA加载问题历史记录可能相当困难。它显着影响API响应时间,尤其是当您有长的变更日志时。这也是您可以通过告诉 \Badoo\Jira\Issue
您需要什么来优化的事情
// 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! }
遗憾的是,您不能同时使用 $fields 和 $expand 参数。这是因为 \Badoo\Jira\Issue
缓存的内部逻辑,这种组合会破坏它。如果出现问题,我们将在未来修复此问题。
使用自定义字段管理API请求
您可以通过几种方式实例化自定义字段对象。与 \Badoo\Jira\Issue
的实例化一样,它们在初始化和值更新所需的API请求方面有所不同。
$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);
在两个示例 CustomField 对象中,我们创建的 CustomField 对象都包含 \Badoo\Jira\Issue 对象。差异在于您开始处理一个问题的几个自定义字段时。
使用静态方法 ::forIssue()
进行初始化始终会在内部创建新的 \Badoo\Jira\Issue 对象。这意味着字段
$MyCustomField1 = \Example\CustomFields\MyFirstCustomField::forIssue('SMPL-1'); $MyCustomField2 = \Example\CustomFields\MySecondCustomField::forIssue('SMPL-1');
将具有不同的 \Badoo\Jira\Issue
对象,即使它们引用的是单个 JIRA 问题。
所有自定义字段都使用 \Badoo\Jira\Issue
作为管理其值的工具:它们通过它加载数据,并使用 Issue 提供的接口进行编辑。
当您调用 $CustomField->setValue()
时,它实际上类似于 $Issue->edit(<自定义字段ID>, <新字段值>);
。
这意味着您可以将多个自定义字段更改“堆叠”在一个 $Issue 对象中,以一次发送更新到API,从而使与API的交互更优化。
$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
使用其他类管理API请求
其他类,如状态、优先级和用户,具有特殊的 ::get
静态方法,该方法类似于常规构造函数,但对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.
扩展 \Badoo\Jira\Issue
\Badoo\Jira\Issue
是关于抽象 JIRA 实例的。它不知道您经常使用的自定义字段、您经常过渡的状态等。拥有您自己的常用操作的快捷方式要方便得多
为此,我们建议创建自己的 Issue 类以扩展 \Badoo\Jira\Issue
功能并添加您自己的方法。
例如,您可能希望通过一个调用轻松关闭问题,设置解决状态为某些特殊值。以下是方法
namespace Example; class Issue extends \Badoo\Jira\Issue { public function markDone() : Issue { return $this; } } // ... $Issue = new \Example\Issue('SMPL-1'); $Issue->markDone();
您可能希望扩展 \Badoo\Jira\Issue\CreateRequest
以返回您的 Issue 对象而不是原始对象
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']); } }
在子类中要使用的方法
这里只是一段代码示例。它们比一大堆文字更有信息量。
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 } }
编写自己的自定义字段基类
所有自定义字段都应该继承自 \Badoo\Jira\Issue\CustomFields\CustomField
类或其子类。自定义字段基类的最简单示例是 \Badoo\Jira\CustomFields\TextField
和 \Badoo\Jira\CustomFields\NumberField
。
您应该了解一些其他特殊方法
$this->getOriginalObject()
- 获取由 JIRA API 提供的字段值。$this->dropCache()
- 删除内部对象缓存,例如删除缓存的字段值。
getOriginalObject()
方法请求当前字段值的绑定 Issue 对象。它在当前对象内部缓存值,因此连续多次调用它是安全的。这不会导致多次 API 请求。我们期望您在从 \Badoo\Jira\Issue\CustomFields\CustomField
直接继承的自定义类中编写代码时,始终使用此方法而不是 $this->Issue->getFieldValue()
。
dropCache()
方法旨在删除对象内部缓存的有关字段值的所有数据。如果您计划在您的自定义类中使用内部属性,请务必重新定义 dropCache()
方法,以便清除您字段的价值。
当绑定 Issue 对象从 API 加载数据后,会调用 dropCache()
方法。这是通知所有现有绑定自定义字段对象字段值可能已更新的方式。