├── composer.json └── src ├── Client.php ├── Contracts └── Transporter.php ├── Enums └── Transporter │ ├── ContentType.php │ └── Method.php ├── Exceptions ├── ErrorException.php ├── TransporterException.php └── UnserializableResponse.php ├── Jira.php ├── Resources ├── Attachments.php ├── Concerns │ └── Transportable.php ├── Customers.php ├── Groups.php ├── Issues.php ├── Requests.php └── Users.php ├── Transporters └── HttpTransporter.php └── ValueObjects ├── BasicAuthentication.php └── Transporter ├── BaseUri.php ├── Headers.php └── Payload.php /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devmoath/jira-php", 3 | "description": "Jira PHP is a supercharged PHP API client that allows you to interact with the Jira API and the Service Desk API", 4 | "keywords": [ 5 | "php", 6 | "jira", 7 | "sdk", 8 | "service-desk", 9 | "api", 10 | "client" 11 | ], 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Moath Alhajri", 16 | "email": "moath.alhajrii@gmail.com" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.1.0", 21 | "guzzlehttp/guzzle": "^7.5.0" 22 | }, 23 | "require-dev": { 24 | "laravel/pint": "^1.2.0", 25 | "mockery/mockery": "^1.6", 26 | "nunomaduro/collision": "^7.0.0", 27 | "pestphp/pest": "^2.0.0", 28 | "phpstan/extension-installer": "^1.2", 29 | "phpstan/phpstan": "^1.8.6", 30 | "phpstan/phpstan-strict-rules": "^1.4", 31 | "symfony/var-dumper": "^6.2.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Jira\\": "src/" 36 | }, 37 | "files": [ 38 | "src/Jira.php" 39 | ] 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Tests\\": "tests/" 44 | } 45 | }, 46 | "minimum-stability": "dev", 47 | "prefer-stable": true, 48 | "config": { 49 | "sort-packages": true, 50 | "preferred-install": "dist", 51 | "allow-plugins": { 52 | "pestphp/pest-plugin": true, 53 | "phpstan/extension-installer": true 54 | } 55 | }, 56 | "scripts": { 57 | "lint": "pint --preset laravel -v --ansi", 58 | "test:lint": "pint --preset laravel --test -v --ansi", 59 | "test:types": "phpstan analyse --ansi", 60 | "test:unit": "pest --colors=always --min=100 --order-by=random --coverage", 61 | "test": [ 62 | "@test:lint", 63 | "@test:types", 64 | "@test:unit" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | transporter); 25 | } 26 | 27 | public function customers(): Customers 28 | { 29 | return new Customers(transporter: $this->transporter); 30 | } 31 | 32 | public function groups(): Groups 33 | { 34 | return new Groups(transporter: $this->transporter); 35 | } 36 | 37 | public function issues(): Issues 38 | { 39 | return new Issues(transporter: $this->transporter); 40 | } 41 | 42 | public function requests(): Requests 43 | { 44 | return new Requests(transporter: $this->transporter); 45 | } 46 | 47 | public function users(): Users 48 | { 49 | return new Users(transporter: $this->transporter); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Contracts/Transporter.php: -------------------------------------------------------------------------------- 1 | |null 16 | * 17 | * @throws \Jira\Exceptions\ErrorException 18 | * @throws \Jira\Exceptions\TransporterException 19 | * @throws \Jira\Exceptions\UnserializableResponse 20 | * @throws \JsonException 21 | */ 22 | public function request(Payload $payload): ?array; 23 | 24 | /** 25 | * @throws \Jira\Exceptions\ErrorException 26 | * @throws \Jira\Exceptions\TransporterException 27 | * @throws \JsonException 28 | */ 29 | public function requestContent(Payload $payload): string; 30 | } 31 | -------------------------------------------------------------------------------- /src/Enums/Transporter/ContentType.php: -------------------------------------------------------------------------------- 1 | getMessage(), 0, $clientException); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Exceptions/UnserializableResponse.php: -------------------------------------------------------------------------------- 1 | getMessage(), 0, $jsonException); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Jira.php: -------------------------------------------------------------------------------- 1 | 20 | * 21 | * @throws \Jira\Exceptions\ErrorException 22 | * @throws \Jira\Exceptions\TransporterException 23 | * @throws \Jira\Exceptions\UnserializableResponse 24 | * @throws \JsonException 25 | */ 26 | public function get(string $id): array 27 | { 28 | $payload = Payload::create(uri: "api/2/attachment/$id"); 29 | 30 | // @phpstan-ignore-next-line 31 | return $this->transporter->request(payload: $payload); 32 | } 33 | 34 | /** 35 | * Remove an attachment. 36 | * 37 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/attachment-removeAttachment 38 | * 39 | * @throws \Jira\Exceptions\ErrorException 40 | * @throws \Jira\Exceptions\TransporterException 41 | * @throws \Jira\Exceptions\UnserializableResponse 42 | * @throws \JsonException 43 | */ 44 | public function remove(string $id): void 45 | { 46 | $payload = Payload::create( 47 | uri: "api/2/attachment/$id", 48 | method: Method::DELETE, 49 | ); 50 | 51 | $this->transporter->request(payload: $payload); 52 | } 53 | 54 | /** 55 | * download an attachment. 56 | * 57 | * @throws \Jira\Exceptions\ErrorException 58 | * @throws \Jira\Exceptions\TransporterException 59 | * @throws \JsonException 60 | */ 61 | public function download(string $url): string 62 | { 63 | $payload = Payload::create(uri: $url); 64 | 65 | return $this->transporter->requestContent(payload: $payload); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Resources/Concerns/Transportable.php: -------------------------------------------------------------------------------- 1 | $body 20 | * @return non-empty-array 21 | * 22 | * @throws \Jira\Exceptions\ErrorException 23 | * @throws \Jira\Exceptions\TransporterException 24 | * @throws \Jira\Exceptions\UnserializableResponse 25 | * @throws \JsonException 26 | */ 27 | public function create(array $body): array 28 | { 29 | $payload = Payload::create( 30 | uri: 'servicedeskapi/customer', 31 | method: Method::POST, 32 | body: $body, 33 | ); 34 | 35 | // @phpstan-ignore-next-line 36 | return $this->transporter->request(payload: $payload); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Resources/Groups.php: -------------------------------------------------------------------------------- 1 | $body 20 | * @return non-empty-array 21 | * 22 | * @throws \Jira\Exceptions\ErrorException 23 | * @throws \Jira\Exceptions\TransporterException 24 | * @throws \Jira\Exceptions\UnserializableResponse 25 | * @throws \JsonException 26 | */ 27 | public function create(array $body): array 28 | { 29 | $payload = Payload::create( 30 | uri: 'api/2/group', 31 | method: Method::POST, 32 | body: $body, 33 | ); 34 | 35 | // @phpstan-ignore-next-line 36 | return $this->transporter->request(payload: $payload); 37 | } 38 | 39 | /** 40 | * Delete a group by given group parameter. 41 | * 42 | * @param non-empty-array $query 43 | * 44 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/group-removeGroup 45 | * 46 | * @throws \Jira\Exceptions\ErrorException 47 | * @throws \Jira\Exceptions\TransporterException 48 | * @throws \Jira\Exceptions\UnserializableResponse 49 | * @throws \JsonException 50 | */ 51 | public function remove(array $query): void 52 | { 53 | $payload = Payload::create( 54 | uri: 'api/2/group', 55 | method: Method::DELETE, 56 | query: $query 57 | ); 58 | 59 | $this->transporter->request(payload: $payload); 60 | } 61 | 62 | /** 63 | * Return a paginated list of users who are members of the specified group and its subgroups. 64 | * 65 | * @param non-empty-array $query 66 | * @return non-empty-array 67 | * 68 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/group-getUsersFromGroup 69 | * 70 | * @throws \Jira\Exceptions\ErrorException 71 | * @throws \Jira\Exceptions\TransporterException 72 | * @throws \Jira\Exceptions\UnserializableResponse 73 | * @throws \JsonException 74 | */ 75 | public function getUsers(array $query): array 76 | { 77 | $payload = Payload::create( 78 | uri: 'api/2/group/member', 79 | query: $query 80 | ); 81 | 82 | // @phpstan-ignore-next-line 83 | return $this->transporter->request(payload: $payload); 84 | } 85 | 86 | /** 87 | * Add given user to a group. 88 | * 89 | * @param non-empty-array $body 90 | * @param non-empty-array $query 91 | * @return non-empty-array 92 | * 93 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/group-addUserToGroup 94 | * 95 | * @throws \Jira\Exceptions\ErrorException 96 | * @throws \Jira\Exceptions\TransporterException 97 | * @throws \Jira\Exceptions\UnserializableResponse 98 | * @throws \JsonException 99 | */ 100 | public function addUser(array $body, array $query): array 101 | { 102 | $payload = Payload::create( 103 | uri: 'api/2/group/user', 104 | method: Method::POST, 105 | body: $body, 106 | query: $query, 107 | ); 108 | 109 | // @phpstan-ignore-next-line 110 | return $this->transporter->request(payload: $payload); 111 | } 112 | 113 | /** 114 | * Remove given user from a group. 115 | * 116 | * @param non-empty-array $query 117 | * 118 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/group-removeUserFromGroup 119 | * 120 | * @throws \Jira\Exceptions\ErrorException 121 | * @throws \Jira\Exceptions\TransporterException 122 | * @throws \Jira\Exceptions\UnserializableResponse 123 | * @throws \JsonException 124 | */ 125 | public function removeUser(array $query): void 126 | { 127 | $payload = Payload::create( 128 | uri: 'api/2/group/user', 129 | method: Method::DELETE, 130 | query: $query 131 | ); 132 | 133 | $this->transporter->request(payload: $payload); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Resources/Issues.php: -------------------------------------------------------------------------------- 1 | $body 21 | * @return non-empty-array 22 | * 23 | * @throws \Jira\Exceptions\ErrorException 24 | * @throws \Jira\Exceptions\TransporterException 25 | * @throws \Jira\Exceptions\UnserializableResponse 26 | * @throws \JsonException 27 | */ 28 | public function create(array $body): array 29 | { 30 | $payload = Payload::create( 31 | uri: 'api/2/issue', 32 | method: Method::POST, 33 | body: $body, 34 | ); 35 | 36 | // @phpstan-ignore-next-line 37 | return $this->transporter->request(payload: $payload); 38 | } 39 | 40 | /** 41 | * Create issues or sub-tasks from a JSON representation. 42 | * 43 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/issue-createIssues 44 | * 45 | * @param non-empty-array $body 46 | * @return non-empty-array 47 | * 48 | * @throws \Jira\Exceptions\ErrorException 49 | * @throws \Jira\Exceptions\TransporterException 50 | * @throws \Jira\Exceptions\UnserializableResponse 51 | * @throws \JsonException 52 | */ 53 | public function bulk(array $body): array 54 | { 55 | $payload = Payload::create( 56 | uri: 'api/2/issue/bulk', 57 | method: Method::POST, 58 | body: $body, 59 | ); 60 | 61 | // @phpstan-ignore-next-line 62 | return $this->transporter->request(payload: $payload); 63 | } 64 | 65 | /** 66 | * Return a full representation of the issue for the given issue key. 67 | * 68 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/issue-getIssue 69 | * 70 | * @param array $query 71 | * @return non-empty-array 72 | * 73 | * @throws \Jira\Exceptions\ErrorException 74 | * @throws \Jira\Exceptions\TransporterException 75 | * @throws \Jira\Exceptions\UnserializableResponse 76 | * @throws \JsonException 77 | */ 78 | public function get(int|string $id, array $query = []): array 79 | { 80 | $payload = Payload::create( 81 | uri: "api/2/issue/$id", 82 | query: $query, 83 | ); 84 | 85 | // @phpstan-ignore-next-line 86 | return $this->transporter->request(payload: $payload); 87 | } 88 | 89 | /** 90 | * Delete an issue. 91 | * 92 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/issue-deleteIssue 93 | * 94 | * @param array $query 95 | * 96 | * @throws \Jira\Exceptions\ErrorException 97 | * @throws \Jira\Exceptions\TransporterException 98 | * @throws \Jira\Exceptions\UnserializableResponse 99 | * @throws \JsonException 100 | */ 101 | public function delete(int|string $id, array $query = []): void 102 | { 103 | $payload = Payload::create( 104 | uri: "api/2/issue/$id", 105 | method: Method::DELETE, 106 | query: $query, 107 | ); 108 | 109 | $this->transporter->request(payload: $payload); 110 | } 111 | 112 | /** 113 | * Edit an issue from a JSON representation. 114 | * 115 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/issue-editIssue 116 | * 117 | * @param non-empty-array $body 118 | * @param array $query 119 | * 120 | * @throws \Jira\Exceptions\ErrorException 121 | * @throws \Jira\Exceptions\TransporterException 122 | * @throws \Jira\Exceptions\UnserializableResponse 123 | * @throws \JsonException 124 | */ 125 | public function edit(int|string $id, array $body, array $query = []): void 126 | { 127 | $payload = Payload::create( 128 | uri: "api/2/issue/$id", 129 | method: Method::PUT, 130 | body: $body, 131 | query: $query, 132 | ); 133 | 134 | $this->transporter->request(payload: $payload); 135 | } 136 | 137 | /** 138 | * Archive an issue. 139 | * 140 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/issue-archiveIssue 141 | * 142 | * @throws \Jira\Exceptions\ErrorException 143 | * @throws \Jira\Exceptions\TransporterException 144 | * @throws \Jira\Exceptions\UnserializableResponse 145 | * @throws \JsonException 146 | */ 147 | public function archive(int|string $id): void 148 | { 149 | $payload = Payload::create( 150 | uri: "api/2/issue/$id/archive", 151 | method: Method::PUT, 152 | ); 153 | 154 | $this->transporter->request(payload: $payload); 155 | } 156 | 157 | /** 158 | * Assign an issue to a user. 159 | * 160 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/issue-assign 161 | * 162 | * @param non-empty-array $body 163 | * 164 | * @throws \Jira\Exceptions\ErrorException 165 | * @throws \Jira\Exceptions\TransporterException 166 | * @throws \Jira\Exceptions\UnserializableResponse 167 | * @throws \JsonException 168 | */ 169 | public function assign(int|string $id, array $body): void 170 | { 171 | $payload = Payload::create( 172 | uri: "api/2/issue/$id/assignee", 173 | method: Method::PUT, 174 | body: $body, 175 | ); 176 | 177 | $this->transporter->request(payload: $payload); 178 | } 179 | 180 | /** 181 | * Return all comments for an issue. 182 | * 183 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/issue-getComments 184 | * 185 | * @param array $query 186 | * @return non-empty-array 187 | * 188 | * @throws \Jira\Exceptions\ErrorException 189 | * @throws \Jira\Exceptions\TransporterException 190 | * @throws \Jira\Exceptions\UnserializableResponse 191 | * @throws \JsonException 192 | */ 193 | public function getComments(int|string $id, array $query = []): array 194 | { 195 | $payload = Payload::create( 196 | uri: "api/2/issue/$id/comment", 197 | query: $query, 198 | ); 199 | 200 | // @phpstan-ignore-next-line 201 | return $this->transporter->request(payload: $payload); 202 | } 203 | 204 | /** 205 | * Add new comment to an issue. 206 | * 207 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/issue-addComment 208 | * 209 | * @param non-empty-array $body 210 | * @param array $query 211 | * @return non-empty-array 212 | * 213 | * @throws \Jira\Exceptions\ErrorException 214 | * @throws \Jira\Exceptions\TransporterException 215 | * @throws \Jira\Exceptions\UnserializableResponse 216 | * @throws \JsonException 217 | */ 218 | public function addComment(int|string $id, array $body, array $query = []): array 219 | { 220 | $payload = Payload::create( 221 | uri: "api/2/issue/$id/comment", 222 | method: Method::POST, 223 | body: $body, 224 | query: $query, 225 | ); 226 | 227 | // @phpstan-ignore-next-line 228 | return $this->transporter->request(payload: $payload); 229 | } 230 | 231 | /** 232 | * Update existing comment using its JSON representation. 233 | * 234 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/issue-updateComment 235 | * 236 | * @param non-empty-array $body 237 | * @param array $query 238 | * @return non-empty-array 239 | * 240 | * @throws \Jira\Exceptions\ErrorException 241 | * @throws \Jira\Exceptions\TransporterException 242 | * @throws \Jira\Exceptions\UnserializableResponse 243 | * @throws \JsonException 244 | */ 245 | public function updateComment(int|string $id, int|string $commentId, array $body, array $query = []): array 246 | { 247 | $payload = Payload::create( 248 | uri: "api/2/issue/$id/comment/$commentId", 249 | method: Method::PUT, 250 | body: $body, 251 | query: $query, 252 | ); 253 | 254 | // @phpstan-ignore-next-line 255 | return $this->transporter->request(payload: $payload); 256 | } 257 | 258 | /** 259 | * Delete an existing comment. 260 | * 261 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/issue-getComment 262 | * 263 | * @param array $query 264 | * 265 | * @throws \Jira\Exceptions\ErrorException 266 | * @throws \Jira\Exceptions\TransporterException 267 | * @throws \Jira\Exceptions\UnserializableResponse 268 | * @throws \JsonException 269 | */ 270 | public function deleteComment(int|string $id, int|string $commentId, array $query = []): void 271 | { 272 | $payload = Payload::create( 273 | uri: "api/2/issue/$id/comment/$commentId", 274 | method: Method::DELETE, 275 | query: $query, 276 | ); 277 | 278 | $this->transporter->request(payload: $payload); 279 | } 280 | 281 | /** 282 | * Return a single comment. 283 | * 284 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/issue-getComment 285 | * 286 | * @param array $query 287 | * @return non-empty-array 288 | * 289 | * @throws \Jira\Exceptions\ErrorException 290 | * @throws \Jira\Exceptions\TransporterException 291 | * @throws \Jira\Exceptions\UnserializableResponse 292 | * @throws \JsonException 293 | */ 294 | public function getComment(int|string $id, int|string $commentId, array $query = []): array 295 | { 296 | $payload = Payload::create( 297 | uri: "api/2/issue/$id/comment/$commentId", 298 | query: $query, 299 | ); 300 | 301 | // @phpstan-ignore-next-line 302 | return $this->transporter->request(payload: $payload); 303 | } 304 | 305 | /** 306 | * Get a list of the transitions possible for this issue by the current user, along with fields that are required and their types. 307 | * 308 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/issue-getTransitions 309 | * 310 | * @param array $query 311 | * @return non-empty-array 312 | * 313 | * @throws \Jira\Exceptions\ErrorException 314 | * @throws \Jira\Exceptions\TransporterException 315 | * @throws \Jira\Exceptions\UnserializableResponse 316 | * @throws \JsonException 317 | */ 318 | public function getTransitions(int|string $id, array $query = []): array 319 | { 320 | $payload = Payload::create( 321 | uri: "api/2/issue/$id/transitions", 322 | query: $query, 323 | ); 324 | 325 | // @phpstan-ignore-next-line 326 | return $this->transporter->request(payload: $payload); 327 | } 328 | 329 | /** 330 | * Perform a transition on an issue. 331 | * 332 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/issue-doTransition 333 | * 334 | * @param non-empty-array $body 335 | * @param array $query 336 | * 337 | * @throws \Jira\Exceptions\ErrorException 338 | * @throws \Jira\Exceptions\TransporterException 339 | * @throws \Jira\Exceptions\UnserializableResponse 340 | * @throws \JsonException 341 | */ 342 | public function doTransition(int|string $id, array $body, array $query = []): void 343 | { 344 | $payload = Payload::create( 345 | uri: "api/2/issue/$id/transitions", 346 | method: Method::POST, 347 | body: $body, 348 | query: $query 349 | ); 350 | 351 | $this->transporter->request(payload: $payload); 352 | } 353 | 354 | /** 355 | * Add one or more attachments to an issue. 356 | * 357 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/issue/%7BissueIdOrKey%7D/attachments-addAttachment 358 | * 359 | * @param non-empty-array $body 360 | * @return non-empty-array 361 | * 362 | * @throws \Jira\Exceptions\ErrorException 363 | * @throws \Jira\Exceptions\TransporterException 364 | * @throws \Jira\Exceptions\UnserializableResponse 365 | * @throws \JsonException 366 | */ 367 | public function attach(int|string $id, array $body): array 368 | { 369 | $payload = Payload::create( 370 | uri: "api/2/issue/$id/attachments", 371 | method: Method::POST, 372 | contentType: ContentType::MULTIPART, 373 | body: $body, 374 | ); 375 | 376 | // @phpstan-ignore-next-line 377 | return $this->transporter->request(payload: $payload); 378 | } 379 | 380 | /** 381 | * Search for issues using JQL. 382 | * 383 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/search-search 384 | * 385 | * @param array $query 386 | * @return non-empty-array 387 | * 388 | * @throws \Jira\Exceptions\ErrorException 389 | * @throws \Jira\Exceptions\TransporterException 390 | * @throws \Jira\Exceptions\UnserializableResponse 391 | * @throws \JsonException 392 | */ 393 | public function search(array $query = []): array 394 | { 395 | $payload = Payload::create( 396 | uri: 'api/2/search', 397 | query: $query 398 | ); 399 | 400 | // @phpstan-ignore-next-line 401 | return $this->transporter->request(payload: $payload); 402 | } 403 | } 404 | -------------------------------------------------------------------------------- /src/Resources/Requests.php: -------------------------------------------------------------------------------- 1 | $body 20 | * @return non-empty-array 21 | * 22 | * @throws \Jira\Exceptions\ErrorException 23 | * @throws \Jira\Exceptions\TransporterException 24 | * @throws \Jira\Exceptions\UnserializableResponse 25 | * @throws \JsonException 26 | */ 27 | public function create(array $body): array 28 | { 29 | $payload = Payload::create( 30 | uri: 'servicedeskapi/request', 31 | method: Method::POST, 32 | body: $body, 33 | ); 34 | 35 | // @phpstan-ignore-next-line 36 | return $this->transporter->request(payload: $payload); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Resources/Users.php: -------------------------------------------------------------------------------- 1 | $body 20 | * @param array $query 21 | * @return non-empty-array 22 | * 23 | * @throws \Jira\Exceptions\ErrorException 24 | * @throws \Jira\Exceptions\TransporterException 25 | * @throws \Jira\Exceptions\UnserializableResponse 26 | * @throws \JsonException 27 | */ 28 | public function update(array $body, array $query = []): array 29 | { 30 | $payload = Payload::create( 31 | uri: 'api/2/user', 32 | method: Method::PUT, 33 | body: $body, 34 | query: $query 35 | ); 36 | 37 | // @phpstan-ignore-next-line 38 | return $this->transporter->request(payload: $payload); 39 | } 40 | 41 | /** 42 | * Create user. 43 | * 44 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/user-createUser 45 | * 46 | * @param non-empty-array $body 47 | * @return non-empty-array 48 | * 49 | * @throws \Jira\Exceptions\ErrorException 50 | * @throws \Jira\Exceptions\TransporterException 51 | * @throws \Jira\Exceptions\UnserializableResponse 52 | * @throws \JsonException 53 | */ 54 | public function create(array $body): array 55 | { 56 | $payload = Payload::create( 57 | uri: 'api/2/user', 58 | method: Method::POST, 59 | body: $body 60 | ); 61 | 62 | // @phpstan-ignore-next-line 63 | return $this->transporter->request(payload: $payload); 64 | } 65 | 66 | /** 67 | * Remove user and its references (like project roles associations, watches, history). 68 | * 69 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/user-removeUser 70 | * 71 | * @param non-empty-array $query 72 | * 73 | * @throws \Jira\Exceptions\ErrorException 74 | * @throws \Jira\Exceptions\TransporterException 75 | * @throws \Jira\Exceptions\UnserializableResponse 76 | * @throws \JsonException 77 | */ 78 | public function remove(array $query): void 79 | { 80 | $payload = Payload::create( 81 | uri: 'api/2/user', 82 | method: Method::DELETE, 83 | query: $query 84 | ); 85 | 86 | $this->transporter->request(payload: $payload); 87 | } 88 | 89 | /** 90 | * Return a user. 91 | * 92 | * @see https://docs.atlassian.com/software/jira/docs/api/REST/8.0.0/#api/2/user-getUser 93 | * 94 | * @param non-empty-array $query 95 | * @return non-empty-array 96 | * 97 | * @throws \Jira\Exceptions\ErrorException 98 | * @throws \Jira\Exceptions\TransporterException 99 | * @throws \Jira\Exceptions\UnserializableResponse 100 | * @throws \JsonException 101 | */ 102 | public function get(array $query): array 103 | { 104 | $payload = Payload::create( 105 | uri: 'api/2/user', 106 | query: $query 107 | ); 108 | 109 | // @phpstan-ignore-next-line 110 | return $this->transporter->request(payload: $payload); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Transporters/HttpTransporter.php: -------------------------------------------------------------------------------- 1 | toRequest(baseUri: $this->baseUri, headers: $this->headers); 34 | 35 | try { 36 | $response = $this->client->sendRequest(request: $request); 37 | } catch (ClientExceptionInterface $clientException) { 38 | throw new TransporterException(clientException: $clientException); 39 | } 40 | 41 | $contents = (string) $response->getBody(); 42 | 43 | if (trim($contents) === '') { 44 | return null; 45 | } 46 | 47 | try { 48 | /** @var non-empty-array $response */ 49 | $response = json_decode(json: $contents, associative: true, flags: JSON_THROW_ON_ERROR); 50 | } catch (JsonException $jsonException) { 51 | throw new UnserializableResponse(jsonException: $jsonException); 52 | } 53 | 54 | $this->hasErrors($response); 55 | 56 | return $response; 57 | } 58 | 59 | public function requestContent(Payload $payload): string 60 | { 61 | $request = $payload->toRequest($this->baseUri, $this->headers); 62 | 63 | try { 64 | $response = $this->client->sendRequest($request); 65 | } catch (ClientExceptionInterface $clientException) { 66 | throw new TransporterException($clientException); 67 | } 68 | 69 | $contents = (string) $response->getBody(); 70 | 71 | try { 72 | /** @var non-empty-array $response */ 73 | $response = json_decode(json: $contents, associative: true, flags: JSON_THROW_ON_ERROR); 74 | 75 | $this->hasErrors($response); 76 | } catch (JsonException) { 77 | // .. 78 | } 79 | 80 | return $contents; 81 | } 82 | 83 | /** 84 | * @param non-empty-array $response 85 | * 86 | * @throws \Jira\Exceptions\ErrorException 87 | */ 88 | private function hasErrors(array $response): void 89 | { 90 | if (isset($response['errorMessage']) && $response['errorMessage'] !== '') { 91 | // @phpstan-ignore-next-line 92 | throw new ErrorException(message: $response['errorMessage']); 93 | } 94 | 95 | if (isset($response['errorMessages']) && $response['errorMessages'] !== []) { 96 | throw new ErrorException(message: $response['errorMessages'][0]); 97 | } 98 | 99 | if (isset($response['errors']) && $response['errors'] !== []) { 100 | throw new ErrorException(message: reset($response['errors'])); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/ValueObjects/BasicAuthentication.php: -------------------------------------------------------------------------------- 1 | username:$this->password"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ValueObjects/Transporter/BaseUri.php: -------------------------------------------------------------------------------- 1 | baseUri; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ValueObjects/Transporter/Headers.php: -------------------------------------------------------------------------------- 1 | $headers 19 | */ 20 | private function __construct(private readonly array $headers) 21 | { 22 | // .. 23 | } 24 | 25 | public static function withAuthorization(BasicAuthentication $basicAuthentication): self 26 | { 27 | return new self(headers: [ 28 | 'Authorization' => (string) $basicAuthentication, 29 | 'X-ExperimentalApi' => 'opt-in', 30 | 'X-Atlassian-Token' => 'no-check', 31 | ]); 32 | } 33 | 34 | public function withContentType(ContentType $contentType, string $suffix = ''): self 35 | { 36 | return new self(headers: [ 37 | ...$this->headers, 38 | 'Content-Type' => $contentType->value.$suffix, 39 | ]); 40 | } 41 | 42 | /** 43 | * @return non-empty-array 44 | */ 45 | public function toArray(): array 46 | { 47 | return $this->headers; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ValueObjects/Transporter/Payload.php: -------------------------------------------------------------------------------- 1 | $body 19 | * @param array $query 20 | */ 21 | private function __construct( 22 | private readonly string $uri, 23 | private readonly Method $method = Method::GET, 24 | private readonly ContentType $contentType = ContentType::JSON, 25 | private readonly array $body = [], 26 | private readonly array $query = [] 27 | ) { 28 | // .. 29 | } 30 | 31 | /** 32 | * @param array $body 33 | * @param array $query 34 | */ 35 | public static function create( 36 | string $uri, 37 | Method $method = Method::GET, 38 | ContentType $contentType = ContentType::JSON, 39 | array $body = [], 40 | array $query = [] 41 | ): self { 42 | return new self( 43 | uri: $uri, 44 | method: $method, 45 | contentType: $contentType, 46 | body: $body, 47 | query: $query, 48 | ); 49 | } 50 | 51 | /** 52 | * @throws \JsonException 53 | */ 54 | public function toRequest(BaseUri $baseUri, Headers $headers): Psr7Request 55 | { 56 | $body = $this->getBody(); 57 | 58 | $query = $this->query !== [] ? '?'.http_build_query(data: $this->query) : ''; 59 | 60 | $headers = $headers->withContentType( 61 | contentType: $this->contentType, 62 | suffix: $body instanceof MultipartStream ? "; boundary={$body->getBoundary()}" : '' 63 | ); 64 | 65 | return new Psr7Request( 66 | method: $this->method->value, 67 | uri: filter_var(value: $this->uri, filter: FILTER_VALIDATE_URL) !== false ? $this->uri.$query : $baseUri.$this->uri.$query, 68 | headers: $headers->toArray(), 69 | body: $body, 70 | ); 71 | } 72 | 73 | private function getBody(): MultipartStream|string|null 74 | { 75 | if ($this->body === []) { 76 | return null; 77 | } 78 | 79 | if ($this->contentType === ContentType::MULTIPART) { 80 | return new MultipartStream(elements: $this->body); 81 | } 82 | 83 | return json_encode(value: $this->body, flags: JSON_THROW_ON_ERROR); 84 | } 85 | } 86 | --------------------------------------------------------------------------------