Passed
Pull Request — master (#37)
by Brian
05:49 queued 02:39
created

JiraProject::getOpenIssues()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 12
nc 6
nop 1
dl 0
loc 20
ccs 13
cts 13
cp 1
crap 5
rs 9.5555
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * JiraProjectReader
6
 */
7
8
namespace Fr3nch13\Jira\Lib;
9
10
use Cake\Core\Configure;
11
use Fr3nch13\Jira\Exception\Exception;
12
use Fr3nch13\Jira\Exception\IssueSubmissionException;
13
use Fr3nch13\Jira\Exception\MissingAllowedTypeException;
14
use Fr3nch13\Jira\Exception\MissingConfigException;
15
use Fr3nch13\Jira\Exception\MissingIssueException;
16
use Fr3nch13\Jira\Exception\MissingIssueFieldException;
17
use Fr3nch13\Jira\Exception\MissingProjectException;
18
use JiraRestApi\Configuration\ArrayConfiguration;
19
use JiraRestApi\Issue\Issue;
20
use JiraRestApi\Issue\IssueField;
21
use JiraRestApi\Issue\IssueService;
22
use JiraRestApi\Issue\JqlQuery;
23
use JiraRestApi\JiraException;
24
use JiraRestApi\Project\ProjectService;
25
26
/**
27
 * Jira Project class
28
 */
29
class JiraProject
30
{
31
    /**
32
     * @var \JiraRestApi\Configuration\ArrayConfiguration Config Object.
33
     */
34
    public $ConfigObj;
35
36
    /**
37
     * @var string|null The key for the project.
38
     */
39
    public $projectKey = null;
40
41
    /**
42
     * @var \JiraRestApi\Project\ProjectService The project service object.
43
     */
44
    public $ProjectService;
45
46
    /**
47
     * @var \JiraRestApi\Project\Project The project object.
48
     */
49
    protected $Project;
50
51
    /**
52
     * @var array<\JiraRestApi\Issue\Version> The list of a Project's Versions.
53
     */
54
    protected $Versions;
55
56
    /**
57
     * @var \JiraRestApi\Issue\IssueService The project service object.
58
     */
59
    public $IssueService;
60
61
    /**
62
     * @var array The Cached list of issues.
63
     */
64
    protected $Issues = [];
65
66
    /**
67
     * @var array The cached list of returned issue info from the below getIssue() method.
68
     */
69
    protected $issuesCache = [];
70
71
    /**
72
     * Valid Types.
73
     * Used to ensure we're getting a valid type when filtering.
74
     * Currently only support Jira Core and Software.
75
     *
76
     * @see https://confluence.atlassian.com/adminjiracloud/issue-types-844500742.html
77
     * @var array
78
     */
79
    protected $validTypes = [
80
        'Bug',
81
        'Epic',
82
        'Story',
83
        'Subtask',
84
        'Task',
85
    ];
86
87
    /**
88
     * @var array Types of issues allowed to be submitted.
89
     */
90
    protected $allowedTypes = [
91
        'Task' => [
92
            'jiraType' => 'Task', // Must be one of the types in the $this->validTypes.
93
            'jiraLabels' => 'task-submitted', // The label used to tag user submitted bugs.
94
            // The form's field information.
95
            'formData' => [
96
                'fields' => [
97
                    'summary' => [
98
                        'type' => 'text',
99
                        'required' => true,
100
                    ],
101
                    'details' => [
102
                        'type' => 'textarea',
103
                        'required' => true,
104
                    ],
105
                ],
106
            ],
107
        ],
108
        'Bug' => [
109
            'jiraType' => 'Bug', // Must be one of the types in the $this->validTypes.
110
            'jiraLabels' => 'bug-submitted', // The label used to tag user submitted bugs.
111
            // The form's field information.
112
            'formData' => [
113
                'fields' => [
114
                    'summary' => [
115
                        'type' => 'text',
116
                        'required' => true,
117
                    ],
118
                    'details' => [
119
                        'type' => 'textarea',
120
                        'required' => true,
121
                    ],
122
                ],
123
            ],
124
        ],
125
        'FeatureRequest' => [
126
            'jiraType' => 'Story', // Must be one of the types in the $this->validTypes.
127
            'jiraLabels' => 'feature-request', // The label used to tag feature requests.
128
            // The form's field information.
129
            'formData' => [
130
                'fields' => [
131
                    'summary' => [
132
                        'type' => 'text',
133
                        'required' => true,
134
                    ],
135
                    'details' => [
136
                        'type' => 'textarea',
137
                        'required' => true,
138
                    ],
139
                ],
140
            ],
141
        ],
142
    ];
143
144
    /**
145
     * This is here for the Form object (or any other object) to use.
146
     * It tacks all errors, even if an exception is thrown.
147
     *
148
     * @var array
149
     */
150
    protected $errors = [];
151
152
    /**
153
     * Constructor
154
     *
155
     * Reads the configuration, and crdate a config object to be passed to the other objects.
156
     *
157
     * @throws \Fr3nch13\Jira\Exception\MissingProjectException When the project can't be found.
158
     * @return void
159
     */
160 28
    public function __construct()
161
    {
162 28
        $this->configure();
163
164
        // setup the objects
165 28
        $this->ProjectService = new ProjectService($this->ConfigObj);
166
        try {
167 28
            $this->Project = $this->ProjectService->get($this->projectKey);
168
        } catch (JiraException $e) {
169
            $this->setError($this->projectKey, 'MissingProjectException');
170
            throw new MissingProjectException($this->projectKey);
171
        }
172
173 28
        $this->Versions = (array)$this->ProjectService->getVersions($this->projectKey);
174 28
        $this->IssueService = new IssueService($this->ConfigObj);
175
    }
176
177
    /**
178
     * Configures the object.
179
     * Broken out of construct.
180
     *
181
     * @throws \Fr3nch13\Jira\Exception\MissingConfigException When a config setting isn't set.
182
     * @return void
183
     */
184 28
    public function configure(): void
185
    {
186 28
        $schema = Configure::read('Jira.schema');
187 28
        if (!$schema) {
188
            $this->setError('schema', 'MissingConfigException');
189
            throw new MissingConfigException('schema');
190
        }
191 28
        $host = Configure::read('Jira.host');
192 28
        if (!$host) {
193
            $this->setError('host', 'MissingConfigException');
194
            throw new MissingConfigException('host');
195
        }
196 28
        $username = Configure::read('Jira.username');
197 28
        if (!$username) {
198
            $this->setError('username', 'MissingConfigException');
199
            throw new MissingConfigException('username');
200
        }
201 28
        $apiKey = Configure::read('Jira.apiKey');
202 28
        if (!$apiKey) {
203
            $this->setError('apiKey', 'MissingConfigException');
204
            throw new MissingConfigException('apiKey');
205
        }
206 28
        $projectKey = Configure::read('Jira.projectKey');
207 28
        if (!$projectKey) {
208
            $this->setError('projectKey', 'MissingConfigException');
209
            throw new MissingConfigException('projectKey');
210
        }
211 28
        $useV3RestApi = Configure::read('Jira.useV3RestApi');
212 28
        if (!$useV3RestApi) {
213
            $this->setError('useV3RestApi', 'MissingConfigException');
214
            throw new MissingConfigException('useV3RestApi');
215
        }
216 28
        $this->ConfigObj = new ArrayConfiguration([
217 28
            'jiraHost' => $schema . '://' . $host,
218
            'jiraUser' => $username,
219
            'jiraPassword' => $apiKey,
220
            'useV3RestApi' => $useV3RestApi,
221
        ]);
222
223 28
        $this->projectKey = $projectKey;
224
    }
225
226
    /**
227
     * Get the Project's Info.
228
     *
229
     * @return \JiraRestApi\Project\Project The information about the project.
230
     * @throws \Fr3nch13\Jira\Exception\MissingProjectException If the project can't be found.
231
     */
232 4
    public function getInfo(): \JiraRestApi\Project\Project
233
    {
234 4
        return $this->Project;
235
    }
236
237
    /**
238
     * Get the Project's Versions.
239
     *
240
     * @return array<\JiraRestApi\Issue\Version> A list of version objects.
241
     */
242 2
    public function getVersions(): array
243
    {
244 2
        return $this->Versions;
245
    }
246
247
    /**
248
     * Get the Project's Issues.
249
     *
250
     * @param string|null $type Filter the Issues by type.
251
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
252
     */
253 4
    public function getIssues(?string $type = null): \JiraRestApi\Issue\IssueSearchResult
254
    {
255 4
        $cacheKey = 'all';
256 4
        if ($type) {
257 2
            $cacheKey .= '-' . $type;
258
        }
259 4
        if (!isset($this->Issues[$cacheKey])) {
260 4
            $jql = new JqlQuery();
261
262 4
            $jql->setProject($this->projectKey);
263 4
            if ($type && in_array($type, $this->validTypes)) {
264 2
                $jql->setType($type);
265
            }
266 4
            $jql->addAnyExpression('ORDER BY key DESC');
267
268 4
            $this->Issues[$cacheKey] = $this->IssueService->search($jql->getQuery(), 0, 1000);
269
        }
270
271 4
        return $this->Issues[$cacheKey];
272
    }
273
274
    /**
275
     * Get the Project's Open Issues.
276
     *
277
     * @param string|null $type Filter the Issues by type.
278
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
279
     */
280 4
    public function getOpenIssues(?string $type = null): \JiraRestApi\Issue\IssueSearchResult
281
    {
282 4
        $cacheKey = 'open';
283 4
        if ($type) {
284 2
            $cacheKey .= '-' . $type;
285
        }
286 4
        if (!isset($this->Issues[$cacheKey])) {
287 4
            $jql = new JqlQuery();
288
289 4
            $jql->setProject($this->projectKey);
290 4
            if ($type && in_array($type, $this->validTypes)) {
291 2
                $jql->setType($type);
292
            }
293 4
            $jql->addAnyExpression('AND resolution is EMPTY');
294 4
            $jql->addAnyExpression('ORDER BY key DESC');
295
296 4
            $this->Issues[$cacheKey] = $this->IssueService->search($jql->getQuery(), 0, 1000);
297
        }
298
299 4
        return $this->Issues[$cacheKey];
300
    }
301
302
    /**
303
     * Gets info on a particular issue within your project.
304
     *
305
     * @param int|null $id The issue id. The integer part without the project key.
306
     * @return \JiraRestApi\Issue\Issue|\JiraRestApi\Issue\IssueV3 the object that has the info of that issue.
307
     * @throws \Fr3nch13\Jira\Exception\Exception If the issue's id isn't given.
308
     * @throws \Fr3nch13\Jira\Exception\MissingIssueException If the project's issue can't be found.
309
     */
310 3
    public function getIssue(?int $id = null): \JiraRestApi\Issue\Issue
311
    {
312 3
        if (!is_int($id)) {
313
            $this->setError(__('Missing the Issue\'s ID.'), 'Exception');
314
            throw new Exception(__('Missing the Issue\'s ID.'));
315
        }
316 3
        $key = $this->projectKey . '-' . $id;
317 3
        if (!isset($this->issuesCache[$key])) {
318
            try {
319 3
                $this->issuesCache[$key] = $this->IssueService->get($key);
320 1
            } catch (JiraException $e) {
321 1
                $this->setError($this->projectKey, 'MissingIssueException');
322 1
                throw new MissingIssueException($key);
323
            }
324
        }
325
326 2
        return $this->issuesCache[$key];
327
    }
328
329
    /**
330
     * Gets a list of issues that are considered bugs.
331
     *
332
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
333
     */
334 2
    public function getBugs(): \JiraRestApi\Issue\IssueSearchResult
335
    {
336 2
        return $this->getIssues('Bug');
337
    }
338
339
    /**
340
     * Gets a list of open issues that are considered bugs.
341
     *
342
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
343
     */
344 2
    public function getOpenBugs(): \JiraRestApi\Issue\IssueSearchResult
345
    {
346 2
        return $this->getOpenIssues('Bug');
347
    }
348
349
    /**
350
     * Methods used to submit an Issue to Jira.
351
     */
352
353
    /**
354
     * Returns the allowed types and their settings
355
     *
356
     * @param string|null $type The type of issue you want to get.
357
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If a type is given, and that type is not configured.
358
     * @return array the content of $this->allowedTypes.
359
     */
360 12
    public function getAllowedTypes(?string $type = null): array
361
    {
362 12
        if ($type) {
363 2
            if (!isset($this->allowedTypes[$type])) {
364
                $this->setError($type, 'MissingAllowedTypeException');
365
                throw new MissingAllowedTypeException($type);
366
            }
367
368 2
            return $this->allowedTypes[$type];
369
        }
370
371 11
        return $this->allowedTypes;
372
    }
373
374
    /**
375
     * Allows you to modify the form allowdTypes to fir your situation.
376
     *
377
     * @param string $type The type of issue you want to add/modify.
378
     * @param array $settings The settings for the type.
379
     * @throws \Fr3nch13\Jira\Exception\MissingIssueFieldException If we're adding a new issue type, and the summary field isn't defined.
380
     * @return void
381
     */
382 5
    public function modifyAllowedTypes(string $type, array $settings = []): void
383
    {
384 5
        if (!isset($this->allowedTypes[$type])) {
385 5
            $this->allowedTypes[$type] = [];
386 5
            if (!isset($settings['jiraType'])) {
387
                $this->setError('jiraType', 'MissingIssueFieldException');
388
                throw new MissingIssueFieldException('jiraType');
389
            }
390 5
            if (!isset($settings['formData'])) {
391
                $this->setError('formData', 'MissingIssueFieldException');
392
                throw new MissingIssueFieldException('formData');
393
            }
394 5
            if (!isset($settings['formData']['fields'])) {
395
                $this->setError('formData.fields', 'MissingIssueFieldException');
396
                throw new MissingIssueFieldException('formData.fields');
397
            }
398 5
            if (!isset($settings['formData']['fields']['summary'])) {
399
                $this->setError('formData.fields.summary', 'MissingIssueFieldException');
400
                throw new MissingIssueFieldException('formData.fields.summary');
401
            }
402
        }
403
404 5
        $this->allowedTypes[$type] += $settings;
405
    }
406
407
    /**
408
     * Checks to see if a type is allowed.
409
     *
410
     * @param string $type The type to check.
411
     * @return bool if it's allowed or not.
412
     */
413 12
    public function isAllowedType(string $type): bool
414
    {
415 12
        return isset($this->allowedTypes[$type]) ? true : false;
416
    }
417
418
    /**
419
     * Gets the array for the forms when submitting an issue to Jira.
420
     *
421
     * @param string|null $type The type of issue we're submitting.
422
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If that type is not configured.
423
     * @throws \Fr3nch13\Jira\Exception\Exception If the form data for that type is missing.
424
     * @return array The array of data to fill in the form with.
425
     */
426 10
    public function getFormData(?string $type = null): array
427
    {
428 10
        if (!$type) {
429
            $this->setError('[$type is not set]', 'MissingAllowedTypeException');
430
            throw new MissingAllowedTypeException('[$type is not set]');
431
        }
432
433 10
        if (!$this->isAllowedType($type)) {
434
            $this->setError($type, 'MissingAllowedTypeException');
435
            throw new MissingAllowedTypeException($type);
436
        }
437
438 10
        $allowedTypes = $this->getAllowedTypes();
439
440 10
        if (!isset($allowedTypes[$type]['formData'])) {
441
            $this->setError('No form data is set.', 'Exception');
442
            throw new Exception(__('No form data is set.'));
443
        }
444
445 10
        return $allowedTypes[$type]['formData'];
446
    }
447
448
    /**
449
     * Sets the formData variable if you want to modify the default/initial values.
450
     *
451
     * @param string $type The type you want to set the data for.
452
     *  - Needs to be in the allowedTypes already.
453
     * @param array $data The definition of the allowed types
454
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If that type is not configured.
455
     * @return void
456
     */
457 9
    public function setFormData(string $type, array $data = []): void
458
    {
459 9
        if (!$type) {
460
            $this->setError('[$type is not set]', 'MissingAllowedTypeException');
461
            throw new MissingAllowedTypeException('[$type is not set]');
462
        }
463
464 9
        if (!$this->isAllowedType($type)) {
465
            $this->setError($type, 'MissingAllowedTypeException');
466
            throw new MissingAllowedTypeException($type);
467
        }
468
469 9
        $this->allowedTypes[$type]['formData'] = $data;
470
    }
471
472
    /**
473
     * Submits the Issue
474
     *
475
     * @param string $type The type you want to set the data for.
476
     *  - Needs to be in the allowedTypes already.
477
     * @param array $data The array of details about the issue.
478
     * @throws \Fr3nch13\Jira\Exception\IssueSubmissionException If submitting the issue fails.
479
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If that issue type is not configured.
480
     * @throws \Fr3nch13\Jira\Exception\MissingIssueFieldException If we're adding a new issue, and required fields aren't defined.
481
     * @return int > 0 If the request was successfully submitted.
482
     */
483 2
    public function submitIssue(string $type, array $data = []): int
484
    {
485 2
        if (!$type) {
486
            $this->setError('[$type is not set]', 'MissingAllowedTypeException');
487
            throw new MissingAllowedTypeException('[$type is not set]');
488
        }
489
490 2
        if (!$this->isAllowedType($type)) {
491
            $this->setError($type, 'MissingAllowedTypeException');
492
            throw new MissingAllowedTypeException($type);
493
        }
494
495 2
        if (!isset($data['summary'])) {
496
            $this->setError('summary', 'MissingIssueFieldException');
497
            throw new MissingIssueFieldException('summary');
498
        }
499
500 2
        $issueField = $this->buildSubmittedIssue($type, $data);
501
502 2
        $issueService = new IssueService($this->ConfigObj);
503
504
        try {
505 2
            $ret = $issueService->create($issueField);
506
        } catch (JiraException $e) {
507
            //Sample return error with json in it.
508
            //Pasting here so I can mock this return message in the unit tests.
509
            //CURL HTTP Request Failed: Status Code : 400, URL:https://[hostname]/rest/api/2/issue
510
            //Error Message : {"errorMessages":[],"errors":{"user_type":"Field 'user_type' cannot be set. It is not on the appropriate screen, or unknown."}}             */
511
            $msg = $e->getMessage();
512
            if (strpos($msg, '{') !== false) {
513
                $msgArray = str_split($msg);
514
                // extract the json message.
515
                $json = '';
516
                $in = 0;
517
                foreach ($msgArray as $i => $char) {
518
                    if ($char == '{') {
519
                        $in++;
520
                    }
521
                    if ($in) {
522
                        $json .= $msg[$i];
523
                    }
524
                    if ($char == '}') {
525
                        $in--;
526
                    }
527
                }
528
                if ($json) {
529
                    $json = json_decode($json, true);
530
                }
531
                if ($json) {
532
                    $newMsg = [];
533
                    if (isset($json['errorMessages'])) {
534
                        foreach ($json['errorMessages'] as $jsonMsg) {
535
                            $newMsg[] = $jsonMsg;
536
                        }
537
                        foreach ($json['errors'] as $jsonMsg) {
538
                            $newMsg[] = $jsonMsg;
539
                        }
540
                        $msg = implode("\n", $newMsg);
541
                    }
542
                }
543
            }
544
            $this->setError($msg, 'IssueSubmissionException');
545
            throw new IssueSubmissionException($msg);
546
        }
547
548 2
        if ($ret instanceof Issue && $ret->id) {
549 2
            return (int)$ret->id;
550
        }
551
552
        return 0;
553
    }
554
555
    /**
556
     * Creates the issue to send to the server.
557
     *
558
     * @param string $type The type of isse we're creating.
559
     * @param array $data The data from the submitted form.
560
     * @throws \Fr3nch13\Jira\Exception\MissingProjectException If submitting the issue fails.
561
     * @return \JiraRestApi\Issue\IssueField
562
     */
563 2
    public function buildSubmittedIssue(string $type, array $data = []): \JiraRestApi\Issue\IssueField
564
    {
565 2
        $typeInfo = $this->getAllowedTypes($type);
566
567
        // make sure we can get the project info first.
568
        // getInfo will throw an exception if it can't find the project.
569
        // putting a try/catch around it so scrutinizer stops complaining.
570
        try {
571 2
            $project = $this->getInfo();
572
        } catch (MissingProjectException $e) {
573
            $this->setError($this->projectKey, 'MissingProjectException');
574
            throw $e;
575
        }
576
577 2
        $issueField = new IssueField();
578 2
        $issueField->setProjectKey($this->projectKey)
579 2
            ->setIssueType($typeInfo['jiraType']);
580
581 2
        if (isset($data['summary'])) {
582 2
            $issueField->setSummary($data['summary']);
583
        }
584 2
        if (isset($data['description'])) {
585 1
            $issueField->setDescription($data['description']);
586
        }
587 2
        if (isset($data['priority'])) {
588
            $issueField->setPriorityName($data['priority']);
589
        }
590 2
        if (isset($data['assignee'])) {
591
            $issueField->setPriorityName($data['assignee']);
592
        }
593 2
        if (isset($data['version'])) {
594
            $issueField->addVersion($data['version']);
595
        }
596 2
        if (isset($data['components'])) {
597
            $issueField->addComponents($data['components']);
598
        }
599 2
        if (isset($data['duedate'])) {
600
            $issueField->setDueDate($data['duedate']);
601
        }
602
603
        // labels should be space seperated
604 2
        if (isset($typeInfo['jiraLabels'])) {
605 2
            if (is_string($typeInfo['jiraLabels'])) {
606 2
                $typeInfo['jiraLabels'] = preg_split('/\s+/', $typeInfo['jiraLabels']);
607
            }
608
            // track the type with a label
609 2
            $typeInfo['jiraLabels'][] = 'user-submitted-type-' . $type;
610 2
            foreach ($typeInfo['jiraLabels'] as $jiralabel) {
611 2
                $issueField->addLabel($jiralabel);
612
            }
613
        }
614
615 2
        return $issueField;
616
    }
617
618
    /**
619
     * Sets an error
620
     *
621
     * @param string $msg The error message.
622
     * @param string $key The key to use in the this->errors array.
623
     * @return bool If saved or not.
624
     */
625 1
    public function setError(string $msg = '', string $key = ''): bool
626
    {
627 1
        if (!trim($msg)) {
628
            return false;
629
        }
630 1
        if ($key) {
631 1
            $this->errors[$key] = $msg;
632
        } else {
633
            $this->errors[] = $msg;
634
        }
635
636 1
        return true;
637
    }
638
639
    /**
640
     * Gets the accumulated error messages.
641
     * If a key is given, return that specific message. If that key doesn't exist, return false.
642
     *
643
     * @param string|null $key The key to the specific message to get.
644
     * @return array|string|false
645
     */
646
    public function getErrors(?string $key = null)
647
    {
648
        if ($key) {
649
            if (isset($this->errors[$key])) {
650
                return $this->errors[$key];
651
            } else {
652
                return false;
653
            }
654
        }
655
656
        return $this->errors;
657
    }
658
}
659