Passed
Pull Request — master (#38)
by Brian
03:55 queued 39s
created

JiraProject::getOpenBugs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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
        $jiraLogFile = Configure::read('Jira.jiraLogFile');
217 28
        if (!$jiraLogFile) {
218
            $this->setError('jiraLogFile', 'MissingConfigException');
219
            throw new MissingConfigException('jiraLogFile');
220
        }
221 28
        $this->ConfigObj = new ArrayConfiguration([
222 28
            'jiraHost' => $schema . '://' . $host,
223
            'jiraUser' => $username,
224
            'jiraPassword' => $apiKey,
225
            'useV3RestApi' => $useV3RestApi,
226
            'jiraLogFile' => $jiraLogFile,
227
        ]);
228
229 28
        $this->projectKey = $projectKey;
230
    }
231
232
    /**
233
     * Get the Project's Info.
234
     *
235
     * @return \JiraRestApi\Project\Project The information about the project.
236
     * @throws \Fr3nch13\Jira\Exception\MissingProjectException If the project can't be found.
237
     */
238 4
    public function getInfo(): \JiraRestApi\Project\Project
239
    {
240 4
        return $this->Project;
241
    }
242
243
    /**
244
     * Get the Project's Versions.
245
     *
246
     * @return array<\JiraRestApi\Issue\Version> A list of version objects.
247
     */
248 2
    public function getVersions(): array
249
    {
250 2
        return $this->Versions;
251
    }
252
253
    /**
254
     * Get the Project's Issues.
255
     *
256
     * @param string|null $type Filter the Issues by type.
257
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
258
     */
259 4
    public function getIssues(?string $type = null): \JiraRestApi\Issue\IssueSearchResult
260
    {
261 4
        $cacheKey = 'all';
262 4
        if ($type) {
263 2
            $cacheKey .= '-' . $type;
264
        }
265 4
        if (!isset($this->Issues[$cacheKey])) {
266 4
            $jql = new JqlQuery();
267
268 4
            $jql->setProject($this->projectKey);
269 4
            if ($type && in_array($type, $this->validTypes)) {
270 2
                $jql->setType($type);
271
            }
272 4
            $jql->addAnyExpression('ORDER BY key DESC');
273
274 4
            $this->Issues[$cacheKey] = $this->IssueService->search($jql->getQuery(), 0, 1000);
275
        }
276
277 4
        return $this->Issues[$cacheKey];
278
    }
279
280
    /**
281
     * Get the Project's Open Issues.
282
     *
283
     * @param string|null $type Filter the Issues by type.
284
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
285
     */
286 4
    public function getOpenIssues(?string $type = null): \JiraRestApi\Issue\IssueSearchResult
287
    {
288 4
        $cacheKey = 'open';
289 4
        if ($type) {
290 2
            $cacheKey .= '-' . $type;
291
        }
292 4
        if (!isset($this->Issues[$cacheKey])) {
293 4
            $jql = new JqlQuery();
294
295 4
            $jql->setProject($this->projectKey);
296 4
            if ($type && in_array($type, $this->validTypes)) {
297 2
                $jql->setType($type);
298
            }
299 4
            $jql->addAnyExpression('AND resolution is EMPTY');
300 4
            $jql->addAnyExpression('ORDER BY key DESC');
301
302 4
            $this->Issues[$cacheKey] = $this->IssueService->search($jql->getQuery(), 0, 1000);
303
        }
304
305 4
        return $this->Issues[$cacheKey];
306
    }
307
308
    /**
309
     * Gets info on a particular issue within your project.
310
     *
311
     * @param int|null $id The issue id. The integer part without the project key.
312
     * @return \JiraRestApi\Issue\Issue|\JiraRestApi\Issue\IssueV3 the object that has the info of that issue.
313
     * @throws \Fr3nch13\Jira\Exception\Exception If the issue's id isn't given.
314
     * @throws \Fr3nch13\Jira\Exception\MissingIssueException If the project's issue can't be found.
315
     */
316 3
    public function getIssue(?int $id = null): \JiraRestApi\Issue\Issue
317
    {
318 3
        if (!is_int($id)) {
319
            $this->setError(__('Missing the Issue\'s ID.'), 'Exception');
320
            throw new Exception(__('Missing the Issue\'s ID.'));
321
        }
322 3
        $key = $this->projectKey . '-' . $id;
323 3
        if (!isset($this->issuesCache[$key])) {
324
            try {
325 3
                $this->issuesCache[$key] = $this->IssueService->get($key);
326 1
            } catch (JiraException $e) {
327 1
                $this->setError($this->projectKey, 'MissingIssueException');
328 1
                throw new MissingIssueException($key);
329
            }
330
        }
331
332 2
        return $this->issuesCache[$key];
333
    }
334
335
    /**
336
     * Gets a list of issues that are considered bugs.
337
     *
338
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
339
     */
340 2
    public function getBugs(): \JiraRestApi\Issue\IssueSearchResult
341
    {
342 2
        return $this->getIssues('Bug');
343
    }
344
345
    /**
346
     * Gets a list of open issues that are considered bugs.
347
     *
348
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
349
     */
350 2
    public function getOpenBugs(): \JiraRestApi\Issue\IssueSearchResult
351
    {
352 2
        return $this->getOpenIssues('Bug');
353
    }
354
355
    /**
356
     * Methods used to submit an Issue to Jira.
357
     */
358
359
    /**
360
     * Returns the allowed types and their settings
361
     *
362
     * @param string|null $type The type of issue you want to get.
363
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If a type is given, and that type is not configured.
364
     * @return array the content of $this->allowedTypes.
365
     */
366 12
    public function getAllowedTypes(?string $type = null): array
367
    {
368 12
        if ($type) {
369 2
            if (!isset($this->allowedTypes[$type])) {
370
                $this->setError($type, 'MissingAllowedTypeException');
371
                throw new MissingAllowedTypeException($type);
372
            }
373
374 2
            return $this->allowedTypes[$type];
375
        }
376
377 11
        return $this->allowedTypes;
378
    }
379
380
    /**
381
     * Allows you to modify the form allowdTypes to fir your situation.
382
     *
383
     * @param string $type The type of issue you want to add/modify.
384
     * @param array $settings The settings for the type.
385
     * @throws \Fr3nch13\Jira\Exception\MissingIssueFieldException If we're adding a new issue type, and the summary field isn't defined.
386
     * @return void
387
     */
388 5
    public function modifyAllowedTypes(string $type, array $settings = []): void
389
    {
390 5
        if (!isset($this->allowedTypes[$type])) {
391 5
            $this->allowedTypes[$type] = [];
392 5
            if (!isset($settings['jiraType'])) {
393
                $this->setError('jiraType', 'MissingIssueFieldException');
394
                throw new MissingIssueFieldException('jiraType');
395
            }
396 5
            if (!isset($settings['formData'])) {
397
                $this->setError('formData', 'MissingIssueFieldException');
398
                throw new MissingIssueFieldException('formData');
399
            }
400 5
            if (!isset($settings['formData']['fields'])) {
401
                $this->setError('formData.fields', 'MissingIssueFieldException');
402
                throw new MissingIssueFieldException('formData.fields');
403
            }
404 5
            if (!isset($settings['formData']['fields']['summary'])) {
405
                $this->setError('formData.fields.summary', 'MissingIssueFieldException');
406
                throw new MissingIssueFieldException('formData.fields.summary');
407
            }
408
        }
409
410 5
        $this->allowedTypes[$type] += $settings;
411
    }
412
413
    /**
414
     * Checks to see if a type is allowed.
415
     *
416
     * @param string $type The type to check.
417
     * @return bool if it's allowed or not.
418
     */
419 12
    public function isAllowedType(string $type): bool
420
    {
421 12
        return isset($this->allowedTypes[$type]) ? true : false;
422
    }
423
424
    /**
425
     * Gets the array for the forms when submitting an issue to Jira.
426
     *
427
     * @param string|null $type The type of issue we're submitting.
428
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If that type is not configured.
429
     * @throws \Fr3nch13\Jira\Exception\Exception If the form data for that type is missing.
430
     * @return array The array of data to fill in the form with.
431
     */
432 10
    public function getFormData(?string $type = null): array
433
    {
434 10
        if (!$type) {
435
            $this->setError('[$type is not set]', 'MissingAllowedTypeException');
436
            throw new MissingAllowedTypeException('[$type is not set]');
437
        }
438
439 10
        if (!$this->isAllowedType($type)) {
440
            $this->setError($type, 'MissingAllowedTypeException');
441
            throw new MissingAllowedTypeException($type);
442
        }
443
444 10
        $allowedTypes = $this->getAllowedTypes();
445
446 10
        if (!isset($allowedTypes[$type]['formData'])) {
447
            $this->setError('No form data is set.', 'Exception');
448
            throw new Exception(__('No form data is set.'));
449
        }
450
451 10
        return $allowedTypes[$type]['formData'];
452
    }
453
454
    /**
455
     * Sets the formData variable if you want to modify the default/initial values.
456
     *
457
     * @param string $type The type you want to set the data for.
458
     *  - Needs to be in the allowedTypes already.
459
     * @param array $data The definition of the allowed types
460
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If that type is not configured.
461
     * @return void
462
     */
463 9
    public function setFormData(string $type, array $data = []): void
464
    {
465 9
        if (!$type) {
466
            $this->setError('[$type is not set]', 'MissingAllowedTypeException');
467
            throw new MissingAllowedTypeException('[$type is not set]');
468
        }
469
470 9
        if (!$this->isAllowedType($type)) {
471
            $this->setError($type, 'MissingAllowedTypeException');
472
            throw new MissingAllowedTypeException($type);
473
        }
474
475 9
        $this->allowedTypes[$type]['formData'] = $data;
476
    }
477
478
    /**
479
     * Submits the Issue
480
     *
481
     * @param string $type The type you want to set the data for.
482
     *  - Needs to be in the allowedTypes already.
483
     * @param array $data The array of details about the issue.
484
     * @throws \Fr3nch13\Jira\Exception\IssueSubmissionException If submitting the issue fails.
485
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If that issue type is not configured.
486
     * @throws \Fr3nch13\Jira\Exception\MissingIssueFieldException If we're adding a new issue, and required fields aren't defined.
487
     * @return int > 0 If the request was successfully submitted.
488
     */
489 2
    public function submitIssue(string $type, array $data = []): int
490
    {
491 2
        if (!$type) {
492
            $this->setError('[$type is not set]', 'MissingAllowedTypeException');
493
            throw new MissingAllowedTypeException('[$type is not set]');
494
        }
495
496 2
        if (!$this->isAllowedType($type)) {
497
            $this->setError($type, 'MissingAllowedTypeException');
498
            throw new MissingAllowedTypeException($type);
499
        }
500
501 2
        if (!isset($data['summary'])) {
502
            $this->setError('summary', 'MissingIssueFieldException');
503
            throw new MissingIssueFieldException('summary');
504
        }
505
506 2
        $issueField = $this->buildSubmittedIssue($type, $data);
507
508 2
        $issueService = new IssueService($this->ConfigObj);
509
510
        try {
511 2
            $ret = $issueService->create($issueField);
512
        } catch (JiraException $e) {
513
            //Sample return error with json in it.
514
            //Pasting here so I can mock this return message in the unit tests.
515
            //CURL HTTP Request Failed: Status Code : 400, URL:https://[hostname]/rest/api/2/issue
516
            //Error Message : {"errorMessages":[],"errors":{"user_type":"Field 'user_type' cannot be set. It is not on the appropriate screen, or unknown."}}             */
517
            $msg = $e->getMessage();
518
            if (strpos($msg, '{') !== false) {
519
                $msgArray = str_split($msg);
520
                // extract the json message.
521
                $json = '';
522
                $in = 0;
523
                foreach ($msgArray as $i => $char) {
524
                    if ($char == '{') {
525
                        $in++;
526
                    }
527
                    if ($in) {
528
                        $json .= $msg[$i];
529
                    }
530
                    if ($char == '}') {
531
                        $in--;
532
                    }
533
                }
534
                if ($json) {
535
                    $json = json_decode($json, true);
536
                }
537
                if ($json) {
538
                    $newMsg = [];
539
                    if (isset($json['errorMessages'])) {
540
                        foreach ($json['errorMessages'] as $jsonMsg) {
541
                            $newMsg[] = $jsonMsg;
542
                        }
543
                        foreach ($json['errors'] as $jsonMsg) {
544
                            $newMsg[] = $jsonMsg;
545
                        }
546
                        $msg = implode("\n", $newMsg);
547
                    }
548
                }
549
            }
550
            $this->setError($msg, 'IssueSubmissionException');
551
            throw new IssueSubmissionException($msg);
552
        }
553
554 2
        if ($ret instanceof Issue && $ret->id) {
555 2
            return (int)$ret->id;
556
        }
557
558
        return 0;
559
    }
560
561
    /**
562
     * Creates the issue to send to the server.
563
     *
564
     * @param string $type The type of isse we're creating.
565
     * @param array $data The data from the submitted form.
566
     * @throws \Fr3nch13\Jira\Exception\MissingProjectException If submitting the issue fails.
567
     * @return \JiraRestApi\Issue\IssueField
568
     */
569 2
    public function buildSubmittedIssue(string $type, array $data = []): \JiraRestApi\Issue\IssueField
570
    {
571 2
        $typeInfo = $this->getAllowedTypes($type);
572
573
        // make sure we can get the project info first.
574
        // getInfo will throw an exception if it can't find the project.
575
        // putting a try/catch around it so scrutinizer stops complaining.
576
        try {
577 2
            $project = $this->getInfo();
578
        } catch (MissingProjectException $e) {
579
            $this->setError($this->projectKey, 'MissingProjectException');
580
            throw $e;
581
        }
582
583 2
        $issueField = new IssueField();
584 2
        $issueField->setProjectKey($this->projectKey)
585 2
            ->setIssueType($typeInfo['jiraType']);
586
587 2
        if (isset($data['summary'])) {
588 2
            $issueField->setSummary($data['summary']);
589
        }
590 2
        if (isset($data['description'])) {
591 1
            $issueField->setDescription($data['description']);
592
        }
593 2
        if (isset($data['priority'])) {
594
            $issueField->setPriorityName($data['priority']);
595
        }
596 2
        if (isset($data['assignee'])) {
597
            $issueField->setPriorityName($data['assignee']);
598
        }
599 2
        if (isset($data['version'])) {
600
            $issueField->addVersion($data['version']);
601
        }
602 2
        if (isset($data['components'])) {
603
            $issueField->addComponents($data['components']);
604
        }
605 2
        if (isset($data['duedate'])) {
606
            $issueField->setDueDate($data['duedate']);
607
        }
608
609
        // labels should be space seperated
610 2
        if (isset($typeInfo['jiraLabels'])) {
611 2
            if (is_string($typeInfo['jiraLabels'])) {
612 2
                $typeInfo['jiraLabels'] = preg_split('/\s+/', $typeInfo['jiraLabels']);
613
            }
614
            // track the type with a label
615 2
            $typeInfo['jiraLabels'][] = 'user-submitted-type-' . $type;
616 2
            foreach ($typeInfo['jiraLabels'] as $jiralabel) {
617 2
                $issueField->addLabel($jiralabel);
618
            }
619
        }
620
621 2
        return $issueField;
622
    }
623
624
    /**
625
     * Sets an error
626
     *
627
     * @param string $msg The error message.
628
     * @param string $key The key to use in the this->errors array.
629
     * @return bool If saved or not.
630
     */
631 1
    public function setError(string $msg = '', string $key = ''): bool
632
    {
633 1
        if (!trim($msg)) {
634
            return false;
635
        }
636 1
        if ($key) {
637 1
            $this->errors[$key] = $msg;
638
        } else {
639
            $this->errors[] = $msg;
640
        }
641
642 1
        return true;
643
    }
644
645
    /**
646
     * Gets the accumulated error messages.
647
     * If a key is given, return that specific message. If that key doesn't exist, return false.
648
     *
649
     * @param string|null $key The key to the specific message to get.
650
     * @return array|string|false
651
     */
652
    public function getErrors(?string $key = null)
653
    {
654
        if ($key) {
655
            if (isset($this->errors[$key])) {
656
                return $this->errors[$key];
657
            } else {
658
                return false;
659
            }
660
        }
661
662
        return $this->errors;
663
    }
664
}
665