toyomsk/jira-client

Jira REST API客户端,为常见API实例(如问题、自定义字段、组件等)提供舒适的封装

dev-master 2023-12-01 10:09 UTC

This package is auto-updated.

Last update: 2024-09-30 01:52:30 UTC


README

简介

这是Badoo JIRA REST客户端。它包含了一组用于最常见API对象的封装类:问题、组件等。

这使代码更容易编写,因为你的IDE将提供自动完成功能。

你还可以生成大量自定义字段的类,以获取你的JIRA安装的文档,直接在PHP代码中。

快速开始

安装

composer require toyomsk/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异常,或解析响应数据。

就是这样,它没有其他复杂的逻辑:你决定请求哪个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库内部的封装类都需要配置API客户端才能工作。它始终作为任何静态方法或封装类构造函数的最后一个参数接收,并且在未提供值时默认为'全局'客户端。

一旦你配置了全局客户端,你就不需要将API客户端提供给所有你初始化的封装器。它们会自己获取它。

\Badoo\Jira\REST\Client::instance()
    ->setJiraUrl('https://jira.example.com')
    ->setAuth('user', 'password/token');

注意:文档中所有后续示例,与JIRA的任何交互都考虑你已经配置了'全局'客户端对象。这就是为什么我们不需要将初始化的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 对象。例如,您可能出于自己的原因存储在您的数据库中一些问题信息:键、摘要和描述。您可以在这些数据上创建 \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客户端实例

大多数包装器类,例如用户、状态、优先级等,都有能力在需要时透明地从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 来获取单个对象。这允许您控制在需要的情况下,如何在 API 错误上获得 \Badoo\Jira\REST\Exception

  • ::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,通过特定请求从 API 获取了一些信息,您仍然可以使用类型化对象而不是原始的 \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 加载。

当您需要问题历史记录时,\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);

在两个示例中,我们创建的自定义字段对象都包含《\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()方法以清除字段值。

dropCache()方法在绑定Issue对象从API加载数据时调用。这是通知所有现有绑定自定义字段对象字段值可能已更新的方法。