diff --git a/README.md b/README.md index 862ad143..92969728 100644 --- a/README.md +++ b/README.md @@ -2470,6 +2470,43 @@ try { } ``` + +#### Get boards +[See Jira API reference](https://developer.atlassian.com/cloud/jira/software/rest/#api-rest-agile-1-0-board-get) + +```php +boardService->getBoards([ + 'startAt' => $startAt, + 'maxResults' => $maxResults + ]); + + $results = [...$results, ...$response->getBoards()]; + + $startAt += $maxResults; + + } while($startAt < $response->total); + + var_dump($results); + +} catch (JiraRestApi\JiraException $e) { + print('Error Occured! ' . $e->getMessage()); +} + +``` + + + #### Get board info [See Jira API reference](https://developer.atlassian.com/cloud/jira/software/rest/#api-rest-agile-1-0-board-boardId-get) diff --git a/composer.json b/composer.json index f3afe122..89cf5176 100644 --- a/composer.json +++ b/composer.json @@ -7,10 +7,8 @@ "php": "^8.0", "ext-curl": "*", "ext-json": "*", - "netresearch/jsonmapper": "^3.0|^4.0|^5.0", - "monolog/monolog": "^2.0|^3.0" - }, - "suggest": { + "netresearch/jsonmapper": "^4.2|^5.0", + "monolog/monolog": "^2.0|^3.0", "vlucas/phpdotenv": "^5.0|^6.0" }, "require-dev": { diff --git a/src/Auth/AuthService.php b/src/Auth/AuthService.php index d28391af..8ed530fa 100644 --- a/src/Auth/AuthService.php +++ b/src/Auth/AuthService.php @@ -80,7 +80,7 @@ public function getSessionCookieValue() * @throws \Exception * @throws \JiraRestApi\JiraException */ - public function __construct(ConfigurationInterface $configuration = null, LoggerInterface $logger = null, $path = './') + public function __construct(?ConfigurationInterface $configuration = null, ?LoggerInterface $logger = null, $path = './') { parent::__construct($configuration, $logger, $path); $this->setAPIUri($this->auth_api_uri); diff --git a/src/Board/BoardService.php b/src/Board/BoardService.php index 07a94179..fb2c5352 100644 --- a/src/Board/BoardService.php +++ b/src/Board/BoardService.php @@ -14,7 +14,7 @@ class BoardService extends \JiraRestApi\JiraClient private $agileVersion = '1.0'; - public function __construct(ConfigurationInterface $configuration = null, LoggerInterface $logger = null, $path = './') + public function __construct(?ConfigurationInterface $configuration = null, ?LoggerInterface $logger = null, $path = './') { parent::__construct($configuration, $logger, $path); $this->setAPIUri('/rest/agile/'.$this->agileVersion); @@ -46,6 +46,31 @@ public function getBoardList($paramArray = []): ?\ArrayObject } } + /** + * Get list of boards with paginated results. + * + * @param array $paramArray + * + * @throws \JiraRestApi\JiraException + * + * @return PaginatedResult|null array of Board class + */ + public function getBoards($paramArray = []): ?PaginatedResult + { + $json = $this->exec($this->uri.$this->toHttpQueryParameter($paramArray), null); + + try { + return $this->json_mapper->map( + json_decode($json, false, 512, $this->getJsonOptions()), + new PaginatedResult() + ); + } catch (\JsonException $exception) { + $this->log->error("Response cannot be decoded from json\nException: {$exception->getMessage()}"); + + return null; + } + } + public function getBoard($id, $paramArray = []): ?Board { $json = $this->exec($this->uri.'/'.$id.$this->toHttpQueryParameter($paramArray), null); @@ -122,6 +147,31 @@ public function getBoardSprints($boardId, $paramArray = []): ?\ArrayObject } } + /** + * Get list of boards with paginated results. + * + * @param array $paramArray + * + * @throws \JiraRestApi\JiraException + * + * @return PaginatedResult|null array of Board class + */ + public function getSprintsForBoard($boardId, $paramArray = []): ?PaginatedResult + { + $json = $this->exec($this->uri.'/'.$boardId.'/sprint'.$this->toHttpQueryParameter($paramArray), null); + + try { + return $this->json_mapper->map( + json_decode($json, false, 512, $this->getJsonOptions()), + new PaginatedResult() + ); + } catch (\JsonException $exception) { + $this->log->error("Response cannot be decoded from json\nException: {$exception->getMessage()}"); + + return null; + } + } + /** * @return \ArrayObject|Epic[]|null */ diff --git a/src/Board/PaginatedResult.php b/src/Board/PaginatedResult.php new file mode 100644 index 00000000..62f62f40 --- /dev/null +++ b/src/Board/PaginatedResult.php @@ -0,0 +1,129 @@ +startAt; + } + + /** + * @param int $startAt + */ + public function setStartAt($startAt) + { + $this->startAt = $startAt; + } + + /** + * @return int + */ + public function getMaxResults() + { + return $this->maxResults; + } + + /** + * @param int $maxResults + */ + public function setMaxResults($maxResults) + { + $this->maxResults = $maxResults; + } + + /** + * @return int + */ + public function getTotal() + { + return $this->total; + } + + /** + * @param int $total + */ + public function setTotal($total) + { + $this->total = $total; + } + + /** + * @return array + */ + public function getValues() + { + return $this->values; + } + + /** + * @param array $values + */ + public function setValues($values) + { + $this->values = $values; + } + + /** + * @param int $index + * + * @return mixed + */ + public function getValue($index) + { + return $this->values[$index]; + } + + /** + * @return string + */ + public function getExpand() + { + return $this->expand; + } + + /** + * @param string $expand + */ + public function setExpand($expand) + { + $this->expand = $expand; + } +} diff --git a/src/Component/Component.php b/src/Component/Component.php index 0efe298b..a5f42081 100644 --- a/src/Component/Component.php +++ b/src/Component/Component.php @@ -25,7 +25,7 @@ class Component implements \JsonSerializable public string $description; - public ?User $lead; + public ?User $lead = null; public string $leadUserName; diff --git a/src/Configuration/AbstractConfiguration.php b/src/Configuration/AbstractConfiguration.php index 993def43..603b28f9 100644 --- a/src/Configuration/AbstractConfiguration.php +++ b/src/Configuration/AbstractConfiguration.php @@ -64,6 +64,11 @@ abstract class AbstractConfiguration implements ConfigurationInterface */ protected ?string $proxyPort = null; + /** + * Proxy type. + */ + protected ?int $proxyType = null; + /** * Proxy user. */ @@ -198,6 +203,11 @@ public function getProxyPort(): ?string return $this->proxyPort; } + public function getProxyType(): ?int + { + return $this->proxyType; + } + public function getProxyUser(): ?string { return $this->proxyUser; diff --git a/src/Configuration/ArrayConfiguration.php b/src/Configuration/ArrayConfiguration.php index df0e6000..267568ba 100644 --- a/src/Configuration/ArrayConfiguration.php +++ b/src/Configuration/ArrayConfiguration.php @@ -1,4 +1,5 @@ curlOptVerbose = $this->env('CURLOPT_VERBOSE', false); $this->proxyServer = $this->env('PROXY_SERVER'); $this->proxyPort = $this->env('PROXY_PORT'); + $this->proxyType = $this->env('PROXY_TYPE'); $this->proxyUser = $this->env('PROXY_USER'); $this->proxyPassword = $this->env('PROXY_PASSWORD'); diff --git a/src/Epic/EpicService.php b/src/Epic/EpicService.php index 6213cca0..5b9df95e 100644 --- a/src/Epic/EpicService.php +++ b/src/Epic/EpicService.php @@ -11,7 +11,7 @@ class EpicService extends \JiraRestApi\JiraClient private $uri = '/epic'; private $version = '1.0'; - public function __construct(ConfigurationInterface $configuration = null, LoggerInterface $logger = null, $path = './') + public function __construct(?ConfigurationInterface $configuration = null, ?LoggerInterface $logger = null, $path = './') { parent::__construct($configuration, $logger, $path); $this->setAPIUri('/rest/agile/'.$this->version); diff --git a/src/Issue/AgileIssueService.php b/src/Issue/AgileIssueService.php index 50ef5511..85bdfcd1 100644 --- a/src/Issue/AgileIssueService.php +++ b/src/Issue/AgileIssueService.php @@ -11,7 +11,7 @@ class AgileIssueService extends \JiraRestApi\JiraClient private $agileVersion = '1.0'; - public function __construct(ConfigurationInterface $configuration = null, LoggerInterface $logger = null, $path = './') + public function __construct(?ConfigurationInterface $configuration = null, ?LoggerInterface $logger = null, $path = './') { parent::__construct($configuration, $logger, $path); $this->setAPIUri('/rest/agile/'.$this->agileVersion); diff --git a/src/Issue/Comment.php b/src/Issue/Comment.php index b7b0b169..67b2577e 100644 --- a/src/Issue/Comment.php +++ b/src/Issue/Comment.php @@ -18,6 +18,9 @@ class Comment implements \JsonSerializable /** @var string */ public $body; + /** @var string */ + public $renderedBody; + /** @var \JiraRestApi\Issue\Reporter */ public $updateAuthor; diff --git a/src/Issue/CustomField.php b/src/Issue/CustomField.php new file mode 100644 index 00000000..3551c7b2 --- /dev/null +++ b/src/Issue/CustomField.php @@ -0,0 +1,41 @@ +issueErrors; + } + + /** + * @param array $issueErrors + */ + public function setIssueErrors($issueErrors) + { + $this->issueErrors = $issueErrors; + } + + /** + * @return Issue[] + */ + public function getIssues() + { + return $this->issues; + } + + /** + * @param Issue[] $issues + */ + public function setIssues($issues) + { + $this->issues = $issues; + } + + /** + * @param int $ndx + * + * @return Issue + */ + public function getIssue($ndx) + { + return $this->issues[$ndx]; + } + + /** + * @return string + */ + public function getExpand() + { + return $this->expand; + } + + /** + * @param string $expand + */ + public function setExpand($expand) + { + $this->expand = $expand; + } +} diff --git a/src/Issue/IssueField.php b/src/Issue/IssueField.php index 2b498b28..3a74424b 100644 --- a/src/Issue/IssueField.php +++ b/src/Issue/IssueField.php @@ -18,11 +18,11 @@ class IssueField implements \JsonSerializable public ?TimeTracking $timeTracking = null; - public ?IssueType $issuetype; + public ?IssueType $issuetype = null; public ?Reporter $reporter = null; - public ?DateTimeInterface $created; + public ?DateTimeInterface $created = null; public ?DateTimeInterface $updated = null; @@ -36,7 +36,7 @@ class IssueField implements \JsonSerializable public Project $project; - public ?string $environment; + public ?string $environment = null; /* @var \JiraRestApi\Issue\Component[] This property must don't describe the type feature for JSON deserialized. */ public $components; @@ -45,15 +45,15 @@ class IssueField implements \JsonSerializable public object $votes; - public ?object $resolution; + public ?object $resolution = null; - public array $fixVersions; + public array $fixVersions = []; - public ?Reporter $creator; + public ?Reporter $creator = null; - public ?object $watches; + public ?object $watches = null; - public ?object $worklog; + public ?object $worklog = null; public ?Reporter $assignee = null; @@ -63,13 +63,13 @@ class IssueField implements \JsonSerializable /** @var \JiraRestApi\Issue\Attachment[] */ public $attachment; - public ?string $aggregatetimespent; + public ?string $aggregatetimespent = null; - public ?string $timeestimate; + public ?string $timeestimate = null; - public ?string $aggregatetimeoriginalestimate; + public ?string $aggregatetimeoriginalestimate = null; - public ?string $resolutiondate; + public ?string $resolutiondate = null; public ?DateTimeInterface $duedate = null; @@ -82,20 +82,20 @@ class IssueField implements \JsonSerializable public int $workratio; - public ?object $aggregatetimeestimate; + public ?object $aggregatetimeestimate = null; - public ?object $aggregateprogress; + public ?object $aggregateprogress = null; - public ?object $lastViewed; + public ?object $lastViewed = null; - public ?object $timeoriginalestimate; + public ?object $timeoriginalestimate = null; /** @var object|null */ public $parent; - public ?array $customFields; + public ?array $customFields = null; - public ?SecurityScheme $security; + public ?SecurityScheme $security = null; public function __construct($updateIssue = false) { @@ -114,7 +114,7 @@ public function __construct($updateIssue = false) public function jsonSerialize(): mixed { $vars = array_filter(get_object_vars($this), function ($var) { - return !is_null($var); + return !empty($var); }); // if assignee property has empty value then remove it. @@ -150,7 +150,7 @@ public function getCustomFields(): ?array return $this->customFields; } - public function addCustomField(string $key, string|int|float|array $value): static + public function addCustomField(string $key, null|string|int|float|array $value): static { $this->customFields[$key] = $value; @@ -355,7 +355,7 @@ public function setParentKeyOrId(string $keyOrId): static return $this; } - public function setParent(Issue $parent): void + public function setParent(?Issue $parent): void { $this->parent = $parent; } diff --git a/src/Issue/IssueSearchResult.php b/src/Issue/IssueSearchResult.php index 3c0cb70c..9b9fafad 100644 --- a/src/Issue/IssueSearchResult.php +++ b/src/Issue/IssueSearchResult.php @@ -1,4 +1,5 @@ startAt; - } - - /** - * @param int $startAt - */ - public function setStartAt($startAt) - { - $this->startAt = $startAt; - } - - /** - * @return int - */ - public function getMaxResults() - { - return $this->maxResults; - } - - /** - * @param int $maxResults - */ - public function setMaxResults($maxResults) - { - $this->maxResults = $maxResults; - } - - /** - * @return int - */ - public function getTotal() + public function getNextPageToken() { - return $this->total; + return $this->nextPageToken; } /** - * @param int $total + * @param string $nextPageToken */ - public function setTotal($total) + public function setNextPageToken($nextPageToken) { - $this->total = $total; + $this->nextPageToken = $nextPageToken; } /** @@ -113,17 +81,14 @@ public function getIssue($ndx) } /** - * @return string + * @return ?string */ - public function getExpand() + public function getExpand(): ?string { return $this->expand; } - /** - * @param string $expand - */ - public function setExpand($expand) + public function setExpand(?string $expand) { $this->expand = $expand; } diff --git a/src/Issue/IssueService.php b/src/Issue/IssueService.php index 82df01a2..15dd07e8 100644 --- a/src/Issue/IssueService.php +++ b/src/Issue/IssueService.php @@ -534,21 +534,22 @@ public function transition($issueIdOrKey, $transition): ?string * * @return IssueSearchResult */ - public function search(string $jql, int $startAt = 0, int $maxResults = 15, array $fields = [], array $expand = [], bool $validateQuery = true): IssueSearchResult + public function search(string $jql, string $nextPageToken = '', int $maxResults = 50, array $fields = [], string $expand = '', array $reconcileIssues = []): IssueSearchResult { - $data = json_encode([ - 'jql' => $jql, - 'startAt' => $startAt, - 'maxResults' => $maxResults, - 'fields' => $fields, - 'expand' => $expand, - 'validateQuery' => $validateQuery, - ]); + $data = [ + 'jql' => $jql, + 'maxResults' => $maxResults, + 'fields' => $fields, + 'expand' => $expand, + 'reconcileIssues' => $reconcileIssues, + ]; - $ret = $this->exec('search', $data, 'POST'); - $json = json_decode($ret); + if ($nextPageToken) { + $data['nextPageToken'] = $nextPageToken; + } - $result = null; + $ret = $this->exec('search/jql', json_encode($data), 'POST'); + $json = json_decode($ret); $result = $this->json_mapper->map( $json, @@ -558,6 +559,33 @@ public function search(string $jql, int $startAt = 0, int $maxResults = 15, arra return $result; } + /** + * Get an approximate count of issues that match a JQL query. + * + * @param string $jql The JQL query string + * + * @throws \JsonMapper_Exception + * @throws JiraException + * + * @return JQLCountResult + */ + public function searchApproximateCount(string $jql): JQLCountResult + { + $data = [ + 'jql' => $jql, + ]; + + $ret = $this->exec('search/approximate-count', json_encode($data), 'POST'); + $json = json_decode($ret); + + $result = $this->json_mapper->map( + $json, + new JQLCountResult() + ); + + return $result; + } + /** * get TimeTracking info. * @@ -700,6 +728,26 @@ public function getWorklogById(string $issueIdOrKey, int $workLogId): Worklog ); } + /** + * @param array $ids + * + * @return array + */ + public function getWorklogsByIds(array $ids): array + { + $ret = $this->exec('/worklog/list', json_encode(['ids' => $ids]), 'POST'); + + $this->log->debug("getWorklogsByIds res=$ret\n"); + + $worklogsResponse = json_decode($ret, false, 512, JSON_THROW_ON_ERROR); + + $worklogs = array_map(function ($worklog) { + return $this->json_mapper->map($worklog, new Worklog()); + }, $worklogsResponse); + + return $worklogs; + } + /** * add work log to issue. * @@ -822,22 +870,26 @@ public function getPriority(int $priorityId): Priority * Get priority by id. * throws HTTPException if the priority is not found, or the calling user does not have permission or view it. * - * @param int $priorityId Id of priority. + * @param $paramArray array parameters * * @throws \JsonMapper_Exception * @throws JiraException * - * @return Priority priority + * @return CustomFieldSearchResult array of CustomeFiled + * + * @see https://docs.atlassian.com/software/jira/docs/api/REST/9.14.0/#api/2/customFields-getCustomFields */ - public function getCustomFields(int $priorityId): Priority + public function getCustomFields(array $paramArray = []): CustomFieldSearchResult { - $ret = $this->exec("priority/$priorityId", null); + $ret = $this->exec('customFields'.$this->toHttpQueryParameter($paramArray), null); $this->log->info('Result='.$ret); + //\JiraRestApi\Dumper::dd(json_decode($ret, false)); + return $this->json_mapper->map( - json_decode($ret), - new Priority() + json_decode($ret, false), + new CustomFieldSearchResult() ); } diff --git a/src/Issue/IssueType.php b/src/Issue/IssueType.php index 5c3b83dd..ce38adb7 100644 --- a/src/Issue/IssueType.php +++ b/src/Issue/IssueType.php @@ -8,7 +8,7 @@ class IssueType implements \JsonSerializable public string $id; - public ?string $description; + public ?string $description = null; public string $iconUrl; diff --git a/src/Issue/JQLCountResult.php b/src/Issue/JQLCountResult.php new file mode 100644 index 00000000..aa748028 --- /dev/null +++ b/src/Issue/JQLCountResult.php @@ -0,0 +1,42 @@ +count; + } + + /** + * Set the count of issues. + * + * @param int $count + */ + public function setCount(int $count): void + { + $this->count = $count; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize(): array + { + return [ + 'count' => $this->count, + ]; + } +} diff --git a/src/Issue/Reporter.php b/src/Issue/Reporter.php index e4d5020f..7322c858 100644 --- a/src/Issue/Reporter.php +++ b/src/Issue/Reporter.php @@ -14,9 +14,9 @@ class Reporter implements \JsonSerializable public string $self; - public ?string $name; + public ?string $name = null; - public ?string $emailAddress; + public ?string $emailAddress = null; public array $avatarUrls; diff --git a/src/Issue/TimeTracking.php b/src/Issue/TimeTracking.php index cc9ec806..bf338da8 100644 --- a/src/Issue/TimeTracking.php +++ b/src/Issue/TimeTracking.php @@ -1,4 +1,5 @@ name = $name; } diff --git a/src/JiraClient.php b/src/JiraClient.php index fd2ee408..02a7dd52 100644 --- a/src/JiraClient.php +++ b/src/JiraClient.php @@ -59,7 +59,7 @@ class JiraClient * * @throws JiraException */ - public function __construct(ConfigurationInterface $configuration = null, LoggerInterface $logger = null, string $path = './') + public function __construct(?ConfigurationInterface $configuration = null, ?LoggerInterface $logger = null, string $path = './') { if ($configuration === null) { if (!file_exists($path.'.env')) { @@ -73,12 +73,22 @@ public function __construct(ConfigurationInterface $configuration = null, Logger $this->json_mapper = new \JsonMapper(); + // Adjust settings for JsonMapper v5.0 BC + if (property_exists($this->json_mapper, 'bStrictNullTypesInArrays')) { + $this->json_mapper->bStrictNullTypesInArrays = false; // if you want to allow nulls in arrays + } + $this->json_mapper->bStrictNullTypes = false; // if you want to allow nulls + $this->json_mapper->bStrictObjectTypeChecking = false; // if you want to disable strict type checking + // Fix "\JiraRestApi\JsonMapperHelper::class" syntax error, unexpected 'class' (T_CLASS), expecting identifier (T_STRING) or variable (T_VARIABLE) or '{' or '$' $this->json_mapper->undefinedPropertyHandler = [new \JiraRestApi\JsonMapperHelper(), 'setUndefinedProperty']; // Properties that are annotated with `@var \DateTimeInterface` should result in \DateTime objects being created. $this->json_mapper->classMap['\\'.\DateTimeInterface::class] = \DateTime::class; + // Just class mapping is not enough, bStrictObjectTypeChecking must be set to false. + $this->json_mapper->bStrictObjectTypeChecking = false; + // create logger if ($this->configuration->getJiraLogEnabled()) { if ($logger) { @@ -185,7 +195,7 @@ protected function filterNullVariable(array $haystack): array * * @return string|bool */ - public function exec(string $context, array|string $post_data = null, string $custom_request = null, string $cookieFile = null): string|bool + public function exec(string $context, array|string|null $post_data = null, ?string $custom_request = null, ?string $cookieFile = null): string|bool { $url = $this->createUrlByContext($context); @@ -195,29 +205,7 @@ public function exec(string $context, array|string $post_data = null, string $cu $this->log->info("Curl $custom_request: $url JsonData=".json_encode($post_data, JSON_UNESCAPED_UNICODE)); } - curl_reset($this->curl); - $ch = $this->curl; - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_URL, $url); - - // post_data - if (!is_null($post_data)) { - // PUT REQUEST - if (!is_null($custom_request) && $custom_request == 'PUT') { - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); - curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); - } - if (!is_null($custom_request) && $custom_request == 'DELETE') { - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - } else { - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); - } - } else { - if (!is_null($custom_request) && $custom_request == 'DELETE') { - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - } - } + $ch = $this->prepareCurlRequest($url, $post_data, $custom_request); // save HTTP Headers $curl_http_headers = [ @@ -236,7 +224,8 @@ public function exec(string $context, array|string $post_data = null, string $cu curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); } - curl_setopt($ch, CURLOPT_ENCODING, ''); + // remove for avoid https://github.com/php/php-src/issues/14184 + //curl_setopt($ch, CURLOPT_ENCODING, ''); curl_setopt( $ch, @@ -435,7 +424,7 @@ protected function createUrlByContext(string $context): string /** * Add authorize to curl request. */ - protected function authorization(\CurlHandle $ch, array &$curl_http_headers, string $cookieFile = null): void + protected function authorization(\CurlHandle $ch, array &$curl_http_headers, ?string $cookieFile = null): void { // use cookie if ($this->getConfiguration()->isCookieAuthorizationEnabled()) { @@ -484,11 +473,14 @@ public function setAPIUri(string $api_uri): string /** * convert to query array to http query parameter. */ - public function toHttpQueryParameter(array $paramArray): string + public function toHttpQueryParameter(array $paramArray, bool $dropNullKey = true): string { $queryParam = '?'; foreach ($paramArray as $key => $value) { + if ($dropNullKey === true && empty($value)) { + continue; + } $v = null; // some param field(Ex: expand) type is array. @@ -507,7 +499,7 @@ public function toHttpQueryParameter(array $paramArray): string /** * download and save into outDir. */ - public function download(string $url, string $outDir, string $file, string $cookieFile = null): mixed + public function download(string $url, string $outDir, string $file, ?string $cookieFile = null): mixed { $curl_http_header = [ 'Accept: */*', @@ -605,6 +597,42 @@ private function proxyConfigCurlHandle(\CurlHandle $ch): void $password = $this->getConfiguration()->getProxyPassword(); curl_setopt($ch, CURLOPT_PROXYUSERPWD, "$username:$password"); } + + // Set the proxy type for curl, default is CURLPROXY_HTTP (0) + if ($this->getConfiguration()->getProxyType()) { + curl_setopt($ch, CURLOPT_PROXYTYPE, $this->getConfiguration()->getProxyType()); + } + } + + private function prepareCurlRequest(string $url, array|string|null $post_data = null, ?string $custom_request = null) + { + curl_reset($this->curl); + $ch = $this->curl; + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->configuration->getTimeout()); + curl_setopt($ch, CURLOPT_TIMEOUT, $this->configuration->getTimeout()); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_URL, $url); + + // post_data + if (!is_null($post_data)) { + // PUT REQUEST + if (!is_null($custom_request) && $custom_request == 'PUT') { + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); + curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); + } + if (!is_null($custom_request) && $custom_request == 'DELETE') { + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); + } else { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); + } + } else { + if (!is_null($custom_request) && $custom_request == 'DELETE') { + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); + } + } + + return $ch; } /** diff --git a/src/JiraException.php b/src/JiraException.php index db14260d..d838a725 100644 --- a/src/JiraException.php +++ b/src/JiraException.php @@ -1,4 +1,5 @@ {$propName} = $jsonValue; $object->customFields[$propName] = $jsonValue; } - } else { + } elseif (isset($object->{$propName})) { $object->{$propName} = $jsonValue; } } diff --git a/src/Project/Project.php b/src/Project/Project.php index e66db698..8f8b3cf8 100644 --- a/src/Project/Project.php +++ b/src/Project/Project.php @@ -28,7 +28,7 @@ class Project implements \JsonSerializable /** * Project key. */ - public ?string $key; + public ?string $key = null; /** * Project name. @@ -73,17 +73,17 @@ class Project implements \JsonSerializable */ public $issueTypes; - public ?string $assigneeType; + public ?string $assigneeType = null; public ?array $versions = []; - public ?array $roles; + public ?array $roles = null; public string $url; public string $projectTypeKey; - public ?string $projectTemplateKey; + public ?string $projectTemplateKey = null; public int $avatarId; diff --git a/src/Project/ProjectService.php b/src/Project/ProjectService.php index 997cf650..10da92cf 100644 --- a/src/Project/ProjectService.php +++ b/src/Project/ProjectService.php @@ -65,7 +65,7 @@ public function get($projectIdOrKey): Project * * @param int|string $projectIdOrKey Project Key * - *@throws \JiraRestApi\JiraException + * @throws \JiraRestApi\JiraException * * @return Reporter[] */ @@ -83,7 +83,7 @@ public function getAssignable(int|string $projectIdOrKey): array /** * @param int|string $projectIdOrKey * - *@throws \JiraRestApi\JiraException + * @throws \JiraRestApi\JiraException * * @return IssueType[] */ @@ -103,7 +103,7 @@ public function getStatuses(int|string $projectIdOrKey): array * * @param int|string $projectIdOrKey * - *@throws \JiraRestApi\JiraException + * @throws \JiraRestApi\JiraException * * @return \JiraRestApi\Component\Component[] */ @@ -123,7 +123,7 @@ public function getProjectComponents(int|string $projectIdOrKey): array * * @param int|string $projectIdOrKey * - *@throws JiraException + * @throws JiraException * * @return array * @return array @@ -174,7 +174,7 @@ public function getProjectTypes(): array /** * @param int|string $key * - *@throws \JsonMapper_Exception + * @throws \JsonMapper_Exception * @throws \JiraRestApi\JiraException * * @return ProjectType @@ -196,7 +196,7 @@ public function getProjectType(int|string $key): ProjectType /** * @param int|string $key * - *@throws \JsonMapper_Exception + * @throws \JsonMapper_Exception * @throws \JiraRestApi\JiraException * * @return ProjectType @@ -221,7 +221,7 @@ public function getAccessibleProjectType(int|string $key): ProjectType * @param int|string $projectIdOrKey * @param array $queryParam * - *@throws \JiraRestApi\JiraException + * @throws \JiraRestApi\JiraException * * @return Version[] array of version */ @@ -279,7 +279,7 @@ public function getVersions(string $projectIdOrKey): \ArrayObject * @param int|string $projectIdOrKey * @param string $versionName * - *@throws \JiraRestApi\JiraException + * @throws \JiraRestApi\JiraException * * @return Version version */ @@ -335,7 +335,7 @@ public function createProject(Project $project): Project * * @param Project $project * - *@throws \JsonMapper_Exception + * @throws \JsonMapper_Exception * @throws JiraException * * @return Project @@ -357,7 +357,7 @@ public function updateProject(Project $project, string|int $projectIdOrKey): Pro /** * @param int|string $projectIdOrKey * - *@throws JiraException + * @throws JiraException * * @return string response status * @@ -378,7 +378,7 @@ public function deleteProject(int|string $projectIdOrKey): string * * @param int|string $projectIdOrKey * - *@throws JiraException + * @throws JiraException * * @return string response status * diff --git a/src/RapidCharts/ScopeChangeBurnDownChartService.php b/src/RapidCharts/ScopeChangeBurnDownChartService.php index 3d065269..63409e76 100644 --- a/src/RapidCharts/ScopeChangeBurnDownChartService.php +++ b/src/RapidCharts/ScopeChangeBurnDownChartService.php @@ -12,7 +12,7 @@ class ScopeChangeBurnDownChartService extends \JiraRestApi\JiraClient private $uri = '/rapid/charts/scopechangeburndownchart'; - public function __construct(ConfigurationInterface $configuration = null, LoggerInterface $logger = null, $path = './') + public function __construct(?ConfigurationInterface $configuration = null, ?LoggerInterface $logger = null, $path = './') { parent::__construct($configuration, $logger, $path); $this->setupAPIUri(); diff --git a/src/Request/RequestService.php b/src/Request/RequestService.php index 0db86d6e..47b65019 100644 --- a/src/Request/RequestService.php +++ b/src/Request/RequestService.php @@ -23,7 +23,7 @@ class RequestService extends \JiraRestApi\JiraClient * @throws JiraException * @throws \Exception */ - public function __construct(ConfigurationInterface $configuration = null, LoggerInterface $logger = null, $path = './') + public function __construct(?ConfigurationInterface $configuration = null, ?LoggerInterface $logger = null, $path = './') { parent::__construct($configuration, $logger, $path); $this->setupAPIUri(); diff --git a/src/ServiceDesk/Customer/Customer.php b/src/ServiceDesk/Customer/Customer.php index ffc6057a..9c6036d1 100644 --- a/src/ServiceDesk/Customer/Customer.php +++ b/src/ServiceDesk/Customer/Customer.php @@ -20,7 +20,7 @@ class Customer implements JsonSerializable public string $displayName; public bool $active; public string $timeZone; - public ?CustomerLinks $_links; + public ?CustomerLinks $_links = null; public string $self; public function setLinks($links): void diff --git a/src/ServiceDesk/Request/Request.php b/src/ServiceDesk/Request/Request.php index 3476520a..bc86923d 100644 --- a/src/ServiceDesk/Request/Request.php +++ b/src/ServiceDesk/Request/Request.php @@ -125,6 +125,13 @@ private function map(object $data, object $target) { $mapper = new JsonMapper(); + // Adjust settings for JsonMapper v5.0 BC + if (property_exists($mapper, 'bStrictNullTypesInArrays')) { + $mapper->bStrictNullTypesInArrays = false; // if you want to allow nulls in arrays + } + $mapper->bStrictNullTypes = false; // if you want to allow nulls + $mapper->bStrictObjectTypeChecking = false; // if you want to disable strict type checking + return $mapper->map( $data, $target diff --git a/src/ServiceDesk/Request/RequestService.php b/src/ServiceDesk/Request/RequestService.php index 37b419a2..ddae9f17 100644 --- a/src/ServiceDesk/Request/RequestService.php +++ b/src/ServiceDesk/Request/RequestService.php @@ -65,7 +65,7 @@ public function getRequestFromJSON(object $jsonData): Request * * @see https://docs.atlassian.com/jira-servicedesk/REST/3.6.2/#servicedeskapi/request-getCustomerRequestByIdOrKey */ - public function get(string $issueId, array $expandParameters = [], Request $request = null): Request + public function get(string $issueId, array $expandParameters = [], ?Request $request = null): Request { $request = ($request) ?: new Request(); @@ -88,7 +88,7 @@ public function get(string $issueId, array $expandParameters = [], Request $requ * * @see https://docs.atlassian.com/jira-servicedesk/REST/3.6.2/#servicedeskapi/request-getMyCustomerRequests */ - public function getRequestsByCustomer(Customer $customer, array $searchParameters, int $serviceDeskId = null): array + public function getRequestsByCustomer(Customer $customer, array $searchParameters, ?int $serviceDeskId = null): array { $defaultSearchParameters = [ 'requestOwnership' => 'OWNED_REQUESTS', @@ -427,6 +427,26 @@ public function getWorklogById(string $issueIdOrKey, int $workLogId): Worklog ); } + /** + * @param array $ids + * + * @return array + */ + public function getWorklogsByIds(array $ids): array + { + $ret = $this->client->exec('/worklog/list', json_encode(['ids' => $ids]), 'POST'); + + $this->logger->debug("getWorklogsByIds res=$ret\n"); + + $worklogsResponse = json_decode($ret, false, 512, JSON_THROW_ON_ERROR); + + $worklogs = array_map(function ($worklog) { + return $this->jsonMapper->map($worklog, new Worklog()); + }, $worklogsResponse); + + return $worklogs; + } + /** * add work log to issue. * diff --git a/src/ServiceDesk/ServiceDeskClient.php b/src/ServiceDesk/ServiceDeskClient.php index 579dcaab..d8809cfe 100644 --- a/src/ServiceDesk/ServiceDeskClient.php +++ b/src/ServiceDesk/ServiceDeskClient.php @@ -12,8 +12,8 @@ class ServiceDeskClient extends JiraClient { public function __construct( - ConfigurationInterface $configuration = null, - LoggerInterface $logger = null, + ?ConfigurationInterface $configuration = null, + ?LoggerInterface $logger = null, string $path = './' ) { parent::__construct($configuration, $logger, $path); diff --git a/src/Sprint/Sprint.php b/src/Sprint/Sprint.php index d0c69732..15020883 100644 --- a/src/Sprint/Sprint.php +++ b/src/Sprint/Sprint.php @@ -4,7 +4,6 @@ namespace JiraRestApi\Sprint; -use DateTimeInterface; use JiraRestApi\JsonSerializableTrait; class Sprint implements \JsonSerializable @@ -29,8 +28,12 @@ class Sprint implements \JsonSerializable public string $originBoardId; + public string $createdDate; + public string $goal; + public array $issues; + public function setNameAsString(string $sprintName): self { $this->name = $sprintName; @@ -52,17 +55,38 @@ public function setOriginBoardIdAsStringOrInt(string|int $originBoardId): self return $this; } - public function setStartDateAsDateTime(DateTimeInterface $startDate, $format = 'Y-m-d'): static + public function setStartDateAsDateTime(\DateTimeInterface $startDate, string $format = 'Y-m-d'): static { $this->startDate = $startDate->format($format); return $this; } - public function setEndDateAsDateTime(DateTimeInterface $endDate, $format = 'Y-m-d'): static + public function setStartDateAsString(string $startDate): static + { + $this->startDate = $startDate; + + return $this; + } + + public function setEndDateAsDateTime(\DateTimeInterface $endDate, string $format = 'Y-m-d'): static { $this->endDate = $endDate->format($format); return $this; } + + public function setEndDateAsString(string $endDate): static + { + $this->endDate = $endDate; + + return $this; + } + + public function setMoveIssues(array $issues): static + { + $this->issues = $issues; + + return $this; + } } diff --git a/src/Sprint/SprintSearchResult.php b/src/Sprint/SprintSearchResult.php index 3c36d524..7d5611de 100644 --- a/src/Sprint/SprintSearchResult.php +++ b/src/Sprint/SprintSearchResult.php @@ -1,4 +1,5 @@ setAPIUri('/rest/agile/1.0'); @@ -79,4 +79,18 @@ public function createSprint(Sprint $sprint): Sprint new Sprint() ); } + + /** + * @see https://docs.atlassian.com/jira-software/REST/9.11.0/#agile/1.0/sprint-moveIssuesToSprint + */ + public function moveIssues2Sprint(int $sprintId, Sprint $sprint): bool + { + $data = json_encode($sprint); + + $ret = $this->exec($this->uri.'/'.$sprintId.'/issue', $data); + + $this->log->debug('moveIssues2Sprint result='.var_export($ret, true)); + + return $ret; + } } diff --git a/src/User/UserService.php b/src/User/UserService.php index b5b4a19f..21db9704 100644 --- a/src/User/UserService.php +++ b/src/User/UserService.php @@ -65,7 +65,7 @@ public function get(array $paramArray): User * * @param array $paramArray * - *@throws \JsonMapper_Exception + * @throws \JsonMapper_Exception * @throws \JiraRestApi\JiraException * * @return User[] @@ -198,7 +198,7 @@ public function getMyself(): User /** * @param array $paramArray * - *@throws \JsonMapper_Exception + * @throws \JsonMapper_Exception * @throws \JiraRestApi\JiraException * * @return User[] diff --git a/src/Version/VersionService.php b/src/Version/VersionService.php index 17512e6b..fee0ab48 100644 --- a/src/Version/VersionService.php +++ b/src/Version/VersionService.php @@ -35,7 +35,7 @@ public function create($version) return $this->json_mapper->map( json_decode($ret), - new Version() + Version::class ); } @@ -71,7 +71,7 @@ public function get(string $id) return $this->json_mapper->map( json_decode($ret), - new Version() + Version::class ); } @@ -100,7 +100,7 @@ public function update(Version $version): Version return $this->json_mapper->map( json_decode($ret), - new Version() + Version::class ); } diff --git a/test-data/reporter-no-email-address.json b/test-data/reporter-no-email-address.json new file mode 100644 index 00000000..b3a05a60 --- /dev/null +++ b/test-data/reporter-no-email-address.json @@ -0,0 +1,16 @@ +{ + "reporter": { + "self": "https://jira.example.com/rest/api/2/user?username=lesstif", + "name": "lesstif", + "key": "lesstif", + "avatarUrls": { + "48x48": "https://secure.gravatar.com/avatar/9f1705ef1d8c977eba04f00556e02922?d=mm&s=48", + "24x24": "https://secure.gravatar.com/avatar/9f1705ef1d8c977eba04f00556e02922?d=mm&s=24", + "16x16": "https://secure.gravatar.com/avatar/9f1705ef1d8c977eba04f00556e02922?d=mm&s=16", + "32x32": "https://secure.gravatar.com/avatar/9f1705ef1d8c977eba04f00556e02922?d=mm&s=32" + }, + "displayName": "정광섭", + "active": true, + "timeZone": "Asia/Seoul" + } +} \ No newline at end of file diff --git a/tests/BoardTest.php b/tests/BoardTest.php index 4eac9308..76aa113f 100644 --- a/tests/BoardTest.php +++ b/tests/BoardTest.php @@ -41,6 +41,31 @@ public function get_all_boards() : string return $last_board_id; } + /** + * @test + * + * Test we can obtain the paginated board list. + */ + public function get_boards() : string + { + $board_service = new BoardService(); + + $board_list = $board_service->getBoards(); + $this->assertInstanceOf(BoardResult::class, $board_list, 'We receive a board list.'); + + $last_board_id = null; + foreach ($board_list->getBoards() as $board) { + $this->assertInstanceOf(Board::class, $board, 'Each element of the list is a Board instance.'); + $this->assertNotNull($board->self, 'self must not null'); + $this->assertNotNull($board->name, 'name must not null'); + $this->assertNotNull($board->type, 'type must not null'); + + $last_board_id = $board->id; + } + + return $last_board_id; + } + /** * @test * diff --git a/tests/CustomFieldsTest.php b/tests/CustomFieldsTest.php index c0818f2b..400b262a 100644 --- a/tests/CustomFieldsTest.php +++ b/tests/CustomFieldsTest.php @@ -2,6 +2,7 @@ namespace JiraRestApi\Test; +use JiraRestApi\Issue\IssueService; use PHPUnit\Framework\TestCase; use JiraRestApi\Dumper; use JiraRestApi\Field\Field; @@ -10,6 +11,37 @@ class CustomFieldsTest extends TestCase { + /** + * @Test + * + * @return array|string[]|void + */ + public function get_customer_field() + { + try { + $iss = new IssueService(); + + $paramArray = [ + 'startAt' => 1, + 'maxResults' => 50, + 'search' => null, + 'projectIds' => [1, 2, 3], + 'screenIds' => null, + 'types' => null, + + 'sortOrder' => null, + 'sortColumn' => null, + 'lastValueUpdate' => null, + ]; + $customerFieldSearchResult = $iss->getCustomFields($paramArray); + + $this->assertLessThan(1, $customerFieldSearchResult->total); + + } catch (JiraException $e) { + $this->assertTrue(false, 'testSearch Failed : '.$e->getMessage()); + } + } + public function testGetFields() { try { @@ -26,6 +58,7 @@ public function testGetFields() return $matches[0]; }, $ret); + $this->assertTrue(true); return $ids; } catch (JiraException $e) { @@ -49,6 +82,7 @@ public function testGetFieldOptions($ids) Dumper::dump($ret); }catch (JiraException $e) {} } + $this->assertTrue(true); } catch (JiraException $e) { $this->assertTrue(false, 'testGetFieldOptions Failed : '.$e->getMessage()); } @@ -69,6 +103,9 @@ public function testCreateFields() $fieldService = new FieldService(); $ret = $fieldService->create($field); + + $this->assertTrue(true); + Dumper::dump($ret); } catch (JiraException $e) { $this->assertTrue(false, 'Field Create Failed : '.$e->getMessage()); diff --git a/tests/MapperTest.php b/tests/MapperTest.php index a74b5e43..49d02452 100644 --- a/tests/MapperTest.php +++ b/tests/MapperTest.php @@ -1,4 +1,4 @@ -mapper->map( + json_decode($ret), new Reporter() + ); + + $this->assertInstanceOf(Reporter::class, $reporter); + + $this->assertEquals('lesstif@gmail.com', $reporter->emailAddress); + } + } diff --git a/tests/SPrintTest.php b/tests/SPrintTest.php index c05b9c8e..3b3c930d 100644 --- a/tests/SPrintTest.php +++ b/tests/SPrintTest.php @@ -71,9 +71,9 @@ public function get_sprints(int $sprintId) : int * @depends get_sprints * * @param int $sprintId - * @return void + * @return int */ - public function get_issues_in_sprints(int $sprintId) + public function get_issues_in_sprints(int $sprintId) : int { try { $sps = new SprintService(); @@ -82,8 +82,40 @@ public function get_issues_in_sprints(int $sprintId) $this->assertNotNull($sprint); Dumper::dump($sprint); + + return $sprintId; } catch (Exception $e) { $this->fail('testSearch Failed : '.$e->getMessage()); } } + + /** + * @test + * @depends get_issues_in_sprints + * + * @param int $sprintId + * @return int + */ + public function move_issues_to_sprints(int $sprintId) : int + { + try { + $sp = (new Sprint()) + ->setMoveIssues([ + "MOBL-1", + "MOBL-5", + ]) + + ; + + $sps = new SprintService(); + + $sprint = $sps->moveIssues2Sprint($sprintId, $sp); + + $this->assertNotNull($sprint); + + return $sprintId; + } catch (Exception $e) { + $this->fail('move_issues_to_sprints Failed : '.$e->getMessage()); + } + } } \ No newline at end of file diff --git a/tests/ServiceDesk/Request/RequestServiceTest.php b/tests/ServiceDesk/Request/RequestServiceTest.php index 2c587ac5..76e831d0 100644 --- a/tests/ServiceDesk/Request/RequestServiceTest.php +++ b/tests/ServiceDesk/Request/RequestServiceTest.php @@ -369,6 +369,33 @@ public function testGetWorklogById(): void self::assertSame($item->timeSpent, $result->timeSpent); } + public function testGetWorklogsByIds(): void + { + $item1 = new stdClass(); + $item1->id = 25; + $item1->timeSpent = '2 hours'; + + $item2 = new stdClass(); + $item2->id = 50; + $item2->timeSpent = '2 hours'; + + + $items = [ + $item1, + $item2, + ]; + + $this->client->method('exec') + ->with("/worklog/list", json_encode(['ids' => [25, 50]]), 'POST') + ->willReturn(json_encode($items)); + + $result = $this->uut->getWorklogsByIds([25, 50]); + + self::assertSame(2, count($result)); + self::assertSame($item1->timeSpent, $result[0]->timeSpent); + + } + public function testAddWorklog(): void { $item = $this->createWorkflow(25, '2 hours'); diff --git a/tests/WorkLogTest.php b/tests/WorkLogTest.php index a393e44a..2da791ea 100644 --- a/tests/WorkLogTest.php +++ b/tests/WorkLogTest.php @@ -95,6 +95,22 @@ public function testGetWorkLogById($workLogid) } } + /** + * @depends testUpdateWorkLogInIssue + */ + public function testGetWorkLogsByIds($workLogid) + { + try { + $issueService = new IssueService(); + + $worklogs = $issueService->getWorklogsByIds([$workLogid]); + + Dumper::dump($worklogs); + } catch (JiraException $e) { + $this->assertTrue(false, 'testGetWorkLogsByIds Failed : '.$e->getMessage()); + } + } + /** * @depends testUpdateWorkLogInIssue */