Passed
Push — 1.x-dev ( bd0048...5a7a02 )
by
unknown
03:10
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
/**
3
 * JiraProject
4
 */
5
6
namespace Fr3nch13\Jira\Lib;
7
8
use Cake\Core\Configure;
9
use Fr3nch13\Jira\Exception\Exception;
10
use Fr3nch13\Jira\Exception\IssueSubmissionException;
11
use Fr3nch13\Jira\Exception\MissingAllowedTypeException;
12
use Fr3nch13\Jira\Exception\MissingConfigException;
13
use Fr3nch13\Jira\Exception\MissingIssueException;
14
use Fr3nch13\Jira\Exception\MissingIssueFieldException;
15
use Fr3nch13\Jira\Exception\MissingProjectException;
16
use JiraRestApi\Configuration\ArrayConfiguration;
17
use JiraRestApi\Issue\IssueField;
18
use JiraRestApi\Issue\IssueService;
19
use JiraRestApi\Issue\JqlQuery;
20
use JiraRestApi\Project\ProjectService;
21
22
/**
23
 * Jira Project class
24
 */
25
class JiraProject
26
{
27
    /**
28
     * Config Object.
29
     * @var \JiraRestApi\Configuration\ArrayConfiguration
30
     */
31
    public $ConfigObj;
32
33
    /**
34
     * The key for the project.
35
     * @var string|null
36
     */
37
    public $projectKey = null;
38
39
    /**
40
     * The project service object.
41
     * @var \JiraRestApi\Project\ProjectService
42
     */
43
    public $ProjectService;
44
45
    /**
46
     * The project object.
47
     * @var \JiraRestApi\Project\Project
48
     */
49
    protected $Project;
50
51
    /**
52
     * The list of a Project's Versions.
53
     * @var array<\JiraRestApi\Issue\Version>
54
     */
55
    protected $Versions;
56
57
    /**
58
     * The project service object.
59
     * @var \JiraRestApi\Issue\IssueService
60
     */
61
    public $IssueService;
62
63
    /**
64
     * The Cached list of issues.
65
     * @var array
66
     */
67
    protected $Issues = [];
68
69
    /**
70
     * The cached list of returned issue info from the below getIssue() method.
71
     * @var array
72
     */
73
    protected $issuesCache = [];
74
75
    /**
76
     * Valid Types.
77
     * Used to ensure we're getting a valid type when filtering.
78
     * Currently only support Jira Core and Software.
79
     * @see https://confluence.atlassian.com/adminjiracloud/issue-types-844500742.html
80
     * @var array
81
     */
82
    protected $validTypes = [
83
        'Bug',
84
        'Epic',
85
        'Story',
86
        'Subtask',
87
        'Task',
88
    ];
89
90
    /**
91
     * Types of issues allowed to be submitted.
92
     * @var array
93
     */
94
    protected $allowedTypes = [
95
        'Task' => [
96
            'jiraType' => 'Task', // Must be one of the types in the $this->validTypes.
97
            'jiraLabels' => 'task-submitted', // The label used to tag user submitted bugs.
98
            // The form's field information.
99
            'formData' => [
100
                'fields' => [
101
                    'summary' => [
102
                        'type' => 'text',
103
                        'required' => true,
104
                    ],
105
                    'description' => [
106
                        'type' => 'textarea',
107
                        'required' => true,
108
                    ],
109
                ],
110
            ],
111
        ],
112
        'Bug' => [
113
            'jiraType' => 'Bug', // Must be one of the types in the $this->validTypes.
114
            'jiraLabels' => 'bug-submitted', // The label used to tag user submitted bugs.
115
            // The form's field information.
116
            'formData' => [
117
                'fields' => [
118
                    'summary' => [
119
                        'type' => 'text',
120
                        'required' => true,
121
                    ],
122
                    'description' => [
123
                        'type' => 'textarea',
124
                        'required' => true,
125
                    ],
126
                ],
127
            ],
128
        ],
129
        'FeatureRequest' => [
130
            'jiraType' => 'Story', // Must be one of the types in the $this->validTypes.
131
            'jiraLabels' => 'feature-request', // The label used to tag feature requests.
132
            // The form's field information.
133
            'formData' => [
134
                'fields' => [
135
                    'summary' => [
136
                        'type' => 'text',
137
                        'required' => true,
138
                    ],
139
                    'description' => [
140
                        'type' => 'textarea',
141
                        'required' => true,
142
                    ],
143
                ],
144
            ],
145
        ],
146
    ];
147
148
    /**
149
     * This is here for the Form object (or any other object) to use.
150
     * It tacks all errors, even if an exception is thrown.
151
     * @var array
152
     */
153
    protected $errors = [];
154
155
    /**
156
     * Constructor
157
     *
158
     * Reads the configuration, and crdate a config object to be passed to the other objects.
159
     *
160
     * @throws \Fr3nch13\Jira\Exception\MissingProjectException When the project can't be found.
161
     * @return void
162
     */
163 26
    public function __construct()
164
    {
165 26
        $this->configure();
166
167
        // setup the objects
168 26
        $this->ProjectService = new ProjectService($this->ConfigObj);
169
        try {
170 26
            $this->Project = $this->ProjectService->get($this->projectKey);
171
        } catch (\JiraRestApi\JiraException $e) {
172
            $this->setError($this->projectKey, 'MissingProjectException');
0 ignored issues
show
Bug introduced by
It seems like $this->projectKey can also be of type null; however, parameter $msg of Fr3nch13\Jira\Lib\JiraProject::setError() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

172
            $this->setError(/** @scrutinizer ignore-type */ $this->projectKey, 'MissingProjectException');
Loading history...
173
            throw new MissingProjectException($this->projectKey);
174
        }
175
176 26
        $this->Versions = (array)$this->ProjectService->getVersions($this->projectKey);
177 26
        $this->IssueService = new IssueService($this->ConfigObj);
178 26
    }
179
180
    /**
181
     * Configures the object.
182
     * Broken out of construct.
183
     *
184
     * @throws \Fr3nch13\Jira\Exception\MissingConfigException When a config setting isn't set.
185
     * @return void
186
     */
187 26
    public function configure()
188
    {
189 26
        $schema = Configure::read('Jira.schema');
190 26
        if (!$schema) {
191
            $this->setError('schema', 'MissingConfigException');
192
            throw new MissingConfigException('schema');
193
        }
194 26
        $host = Configure::read('Jira.host');
195 26
        if (!$host) {
196
            $this->setError('host', 'MissingConfigException');
197
            throw new MissingConfigException('host');
198
        }
199 26
        $username = Configure::read('Jira.username');
200 26
        if (!$username) {
201
            $this->setError('username', 'MissingConfigException');
202
            throw new MissingConfigException('username');
203
        }
204 26
        $apiKey = Configure::read('Jira.apiKey');
205 26
        if (!$apiKey) {
206
            $this->setError('apiKey', 'MissingConfigException');
207
            throw new MissingConfigException('apiKey');
208
        }
209 26
        $projectKey = Configure::read('Jira.projectKey');
210 26
        if (!$projectKey) {
211
            $this->setError('projectKey', 'MissingConfigException');
212
            throw new MissingConfigException('projectKey');
213
        }
214 26
        $this->ConfigObj = new ArrayConfiguration([
215 26
            'jiraHost' => $schema . '://' . $host,
216 26
            'jiraUser' => $username,
217 26
            'jiraPassword' => $apiKey,
218
        ]);
219
220 26
        $this->projectKey = $projectKey;
221 26
    }
222
223
    /**
224
     * Get the Project's Info.
225
     *
226
     * @return \JiraRestApi\Project\Project The information about the project.
227
     * @throws \Fr3nch13\Jira\Exception\MissingProjectException If the project can't be found.
228
     */
229 4
    public function getInfo()
230
    {
231 4
        return $this->Project;
232
    }
233
234
    /**
235
     * Get the Project's Versions.
236
     *
237
     * @return array<\JiraRestApi\Issue\Version> A list of version objects.
238
     */
239 2
    public function getVersions(): array
240
    {
241 2
        return $this->Versions;
242
    }
243
244
    /**
245
     * Get the Project's Issues.
246
     *
247
     * @param string|null $type Filter the Issues by type.
248
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
249
     */
250 4
    public function getIssues($type = null)
251
    {
252 4
        $cacheKey = 'all';
253 4
        if ($type) {
254 2
            $cacheKey .= '-' . $type;
255
        }
256 4
        if (!isset($this->Issues[$cacheKey])) {
257 4
            $jql = new JqlQuery();
258
259 4
            $jql->setProject($this->projectKey);
260 4
            if ($type && in_array($type, $this->validTypes)) {
261 2
                $jql->setType($type);
262
            }
263 4
            $jql->addAnyExpression('ORDER BY key DESC');
264
265 4
            $this->Issues[$cacheKey] = $this->IssueService->search($jql->getQuery(), 0, 1000);
266
        }
267
268 4
        return $this->Issues[$cacheKey];
269
    }
270
271
    /**
272
     * Get the Project's Open Issues.
273
     *
274
     * @param string|null $type Filter the Issues by type.
275
     * @return \JiraRestApi\Issue\IssueSearchResult|\JiraRestApi\Issue\IssueSearchResultV3 A list of issue objects.
276
     */
277 4
    public function getOpenIssues($type = null)
278
    {
279 4
        $cacheKey = 'open';
280 4
        if ($type) {
281 2
            $cacheKey .= '-' . $type;
282
        }
283 4
        if (!isset($this->Issues[$cacheKey])) {
284 4
            $jql = new JqlQuery();
285
286 4
            $jql->setProject($this->projectKey);
287 4
            if ($type && in_array($type, $this->validTypes)) {
288 2
                $jql->setType($type);
289
            }
290 4
            $jql->addAnyExpression('AND resolution is EMPTY');
291 4
            $jql->addAnyExpression('ORDER BY key DESC');
292
293 4
            $this->Issues[$cacheKey] = $this->IssueService->search($jql->getQuery(), 0, 1000);
294
        }
295
296 4
        return $this->Issues[$cacheKey];
297
    }
298
299
    /**
300
     * Gets info on a particular issue within your project.
301
     *
302
     * @param int $id The issue id. The integer part without the project key.
303
     * @return \JiraRestApi\Issue\Issue|\JiraRestApi\Issue\IssueV3 the object that has the info of that issue.
304
     * @throws \Fr3nch13\Jira\Exception\Exception If the issue's id isn't given.
305
     * @throws \Fr3nch13\Jira\Exception\MissingIssueException If the project's issue can't be found.
306
     */
307 2
    public function getIssue(int $id): \JiraRestApi\Issue\Issue
308
    {
309 2
        $key = $this->projectKey . '-' . $id;
310 2
        if (!isset($this->issuesCache[$key])) {
311 2
            $this->issuesCache[$key] = $this->IssueService->get($key);
312 2
            if (!$this->issuesCache[$key]) {
313
                $this->setError($key, 'MissingIssueException');
314
                throw new MissingIssueException($key);
315
            }
316
        }
317
318 2
        return $this->issuesCache[$key];
319
    }
320
321
    /**
322
     * Gets a list of issues that are considered bugs.
323
     * @return \JiraRestApi\Issue\IssueSearchResult A list of issue objects.
324
     */
325 2
    public function getBugs(): \JiraRestApi\Issue\IssueSearchResult
326
    {
327 2
        return $this->getIssues('Bug');
328
    }
329
330
    /**
331
     * Gets a list of open issues that are considered bugs.
332
     * @return \JiraRestApi\Issue\IssueSearchResult A list of issue objects.
333
     */
334 2
    public function getOpenBugs(): \JiraRestApi\Issue\IssueSearchResult
335
    {
336 2
        return $this->getOpenIssues('Bug');
337
    }
338
339
    /**
340
     * Methods used to submit an Issue to Jira.
341
     */
342
343
    /**
344
     * Returns the allowed types and their settings
345
     *
346
     * @param string|null $type The type of issue you want to get.
347
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If a type is given, and that type is not configured.
348
     * @return array the content of $this->allowedTypes.
349
     */
350 11
    public function getAllowedTypes(?string $type = null): array
351
    {
352 11
        if ($type) {
353 2
            if (!isset($this->allowedTypes[$type])) {
354
                $this->setError($type, 'MissingAllowedTypeException');
355
                throw new MissingAllowedTypeException($type);
356
            }
357
358 2
            return $this->allowedTypes[$type];
359
        }
360
361 10
        return $this->allowedTypes;
362
    }
363
364
    /**
365
     * Allows you to modify the form allowdTypes to fir your situation.
366
     *
367
     * @param string $type The type of issue you want to add/modify.
368
     * @param array $settings The settings for the type.
369
     * @throws \Fr3nch13\Jira\Exception\MissingIssueFieldException If we're adding a new issue type, and the summary field isn't defined.
370
     * @return void
371
     */
372 4
    public function modifyAllowedTypes(string $type, array $settings = []): void
373
    {
374 4
        if (!isset($this->allowedTypes[$type])) {
375 4
            $this->allowedTypes[$type] = [];
376 4
            if (!isset($settings['jiraType'])) {
377
                $this->setError('jiraType', 'MissingIssueFieldException');
378
                throw new MissingIssueFieldException('jiraType');
379
            }
380 4
            if (!isset($settings['formData'])) {
381
                $this->setError('formData', 'MissingIssueFieldException');
382
                throw new MissingIssueFieldException('formData');
383
            }
384 4
            if (!isset($settings['formData']['fields'])) {
385
                $this->setError('formData.fields', 'MissingIssueFieldException');
386
                throw new MissingIssueFieldException('formData.fields');
387
            }
388 4
            if (!isset($settings['formData']['fields']['summary'])) {
389
                $this->setError('formData.fields.summary', 'MissingIssueFieldException');
390
                throw new MissingIssueFieldException('formData.fields.summary');
391
            }
392
        }
393
394 4
        $this->allowedTypes[$type] += $settings;
395 4
    }
396
397
    /**
398
     * Checks to see if a type is allowed.
399
     *
400
     * @param string $type The type to check.
401
     * @return bool if it's allowed or not.
402
     */
403 11
    public function isAllowedType(string $type): bool
404
    {
405 11
        return (isset($this->allowedTypes[$type]) ? true : false);
406
    }
407
408
    /**
409
     * Gets the array for the forms when submitting an issue to Jira.
410
     *
411
     * @param string|null $type The type of issue we're submitting.
412
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If that type is not configured.
413
     * @throws \Fr3nch13\Jira\Exception\Exception If the form data for that type is missing.
414
     * @return array The array of data to fill in the form with.
415
     */
416 9
    public function getFormData(?string $type = null): array
417
    {
418 9
        if (!$type) {
419
            $this->setError('[$type is not set]', 'MissingAllowedTypeException');
420
            throw new MissingAllowedTypeException('[$type is not set]');
421
        }
422
423 9
        if (!$this->isAllowedType($type)) {
424
            $this->setError($type, 'MissingAllowedTypeException');
425
            throw new MissingAllowedTypeException($type);
426
        }
427
428 9
        $allowedTypes = $this->getAllowedTypes();
429
430 9
        if (!isset($allowedTypes[$type]['formData'])) {
431
            $this->setError('No form data is set.', 'Exception');
432
            throw new Exception(__('No form data is set.'));
433
        }
434
435 9
        return $allowedTypes[$type]['formData'];
436
    }
437
438
    /**
439
     * Sets the formData variable if you want to modify the default/initial values.
440
     *
441
     * @param string $type The type you want to set the data for.
442
     *  - Needs to be in the allowedTypes already.
443
     * @param array $data The definition of the allowed types
444
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If that type is not configured.
445
     * @return void
446
     */
447 8
    public function setFormData(string $type, array $data = []): void
448
    {
449 8
        if (!$type) {
450
            $this->setError('[$type is not set]', 'MissingAllowedTypeException');
451
            throw new MissingAllowedTypeException('[$type is not set]');
452
        }
453
454 8
        if (!$this->isAllowedType($type)) {
455
            $this->setError($type, 'MissingAllowedTypeException');
456
            throw new MissingAllowedTypeException($type);
457
        }
458
459 8
        $this->allowedTypes[$type]['formData'] = $data;
460 8
    }
461
462
    /**
463
     * Submits the Issue
464
     *
465
     * @param string $type The type you want to set the data for.
466
     *  - Needs to be in the allowedTypes already.
467
     * @param array $data The array of details about the issue.
468
     * @throws \Fr3nch13\Jira\Exception\IssueSubmissionException If submitting the issue fails.
469
     * @throws \Fr3nch13\Jira\Exception\MissingAllowedTypeException If that issue type is not configured.
470
     * @throws \Fr3nch13\Jira\Exception\MissingIssueFieldException If we're adding a new issue, and required fields aren't defined.
471
     * @return bool If the request was successfully submitted.
472
     */
473 2
    public function submitIssue(string $type, array $data = []): bool
474
    {
475 2
        if (!$this->isAllowedType($type)) {
476
            $this->setError($type, 'MissingAllowedTypeException');
477
            throw new MissingAllowedTypeException($type);
478
        }
479
480 2
        if (!isset($data['summary'])) {
481
            $this->setError('summary', 'MissingIssueFieldException');
482
            throw new MissingIssueFieldException('summary');
483
        }
484
485 2
        $issueField = $this->buildSubmittedIssue($type, $data);
486
487 2
        $issueService = new IssueService($this->ConfigObj);
488
489
        try {
490 2
            $ret = $issueService->create($issueField);
491
        } catch (\JiraRestApi\JiraException $e) {
492
            //Sample return error with json in it.
493
            //Pasting here so I can mock this return message in the unit tests.
494
            //CURL HTTP Request Failed: Status Code : 400, URL:https://[hostname]/rest/api/2/issue
495
            //Error Message : {"errorMessages":[],"errors":{"user_type":"Field 'user_type' cannot be set. It is not on the appropriate screen, or unknown."}}             */
496
            $msg = $e->getMessage();
497
            if (strpos($msg, '{') !== false) {
498
                $msgArray = str_split($msg);
499
                // extract the json message.
500
                $json = '';
501
                $in = 0;
502
                foreach ($msgArray as $i => $char) {
503
                    if ($char == '{') {
504
                        $in++;
505
                    }
506
                    if ($in) {
507
                        $json .= $msg[$i];
508
                    }
509
                    if ($char == '}') {
510
                        $in--;
511
                    }
512
                }
513
                if ($json) {
514
                    $json = json_decode($json, true);
515
                }
516
                if ($json) {
517
                    $newMsg = [];
518
                    if (isset($json['errorMessages'])) {
519
                        foreach ($json['errorMessages'] as $jsonMsg) {
520
                            $newMsg[] = $jsonMsg;
521
                        }
522
                        foreach ($json['errors'] as $jsonMsg) {
523
                            $newMsg[] = $jsonMsg;
524
                        }
525
                        $msg = implode("\n", $newMsg);
526
                    }
527
                }
528
            }
529
            $this->setError($msg, 'IssueSubmissionException');
530
            throw new IssueSubmissionException($msg);
531
        }
532
533 2
        if ($ret instanceof \JiraRestApi\Issue\Issue && (int)$ret->id > 0) {
534 2
            return true;
535
        }
536
537
        return false;
538
    }
539
540
    /**
541
     * Creates the issue to send to the server.
542
     *
543
     * @param string $type The type of isse we're creating.
544
     * @param array $data The data from the submitted form.
545
     * @throws \Fr3nch13\Jira\Exception\MissingProjectException If submitting the issue fails.
546
     * @return \JiraRestApi\Issue\IssueField
547
     */
548 2
    public function buildSubmittedIssue(string $type, array $data = []): \JiraRestApi\Issue\IssueField
549
    {
550 2
        $typeInfo = $this->getAllowedTypes($type);
551
        // make sure we can get the project info first.
552
        // getInfo will throw an exception if it can't find the project.
553
        // putting a try/catch around it so scrutinizer stops complaining.
554
        try {
555 2
            $project = $this->getInfo();
556
        } catch (MissingProjectException $e) {
557
            $this->setError($this->projectKey, 'MissingProjectException');
0 ignored issues
show
Bug introduced by
It seems like $this->projectKey can also be of type null; however, parameter $msg of Fr3nch13\Jira\Lib\JiraProject::setError() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

557
            $this->setError(/** @scrutinizer ignore-type */ $this->projectKey, 'MissingProjectException');
Loading history...
558
            throw $e;
559
        }
560
561 2
        $issueField = new IssueField();
562 2
        $issueField->setProjectKey($this->projectKey)
563 2
            ->setIssueType($typeInfo['jiraType']);
564
565 2
        if (isset($data['summary'])) {
566 2
            $issueField->setSummary($data['summary']);
567
        }
568 2
        if (isset($data['description'])) {
569 2
            if (isset($data['referer'])) {
570
                $data['description'] = __("{0}\n\n-----------\n\nReferer: {1}", [$data['description'], $data['referer']]);
571
            }
572 2
            if (isset($data['username'])) {
573
                $data['description'] = __("{0}\n\nUsername: {1}", [$data['description'], $data['username']]);
574
            }
575 2
            $issueField->setDescription($data['description']);
576
        }
577 2
        if (isset($data['priority'])) {
578
            $issueField->setPriorityName($data['priority']);
579
        }
580 2
        if (isset($data['assignee'])) {
581
            $issueField->setPriorityName($data['assignee']);
582
        }
583 2
        if (isset($data['version'])) {
584
            $issueField->addVersion($data['version']);
585
        }
586 2
        if (isset($data['components'])) {
587
            $issueField->addComponents($data['components']);
588
        }
589 2
        if (isset($data['duedate'])) {
590
            $issueField->setDueDate($data['duedate']);
591
        }
592
593
        // labels should be space seperated
594 2
        if (isset($typeInfo['jiraLabels'])) {
595 2
            if (is_string($typeInfo['jiraLabels'])) {
596 2
                $typeInfo['jiraLabels'] = preg_split('/\s+/', $typeInfo['jiraLabels']);
597
            }
598
            // track the type with a label
599 2
            $typeInfo['jiraLabels'][] = 'user-submitted-type-' . $type;
600 2
            foreach ($typeInfo['jiraLabels'] as $jiralabel) {
601 2
                $issueField->addLabel($jiralabel);
602
            }
603
        }
604
605 2
        return $issueField;
606
    }
607
608
    /**
609
     * Sets an error
610
     *
611
     * @param string $msg The error message.
612
     * @param string $key The key to use in the this->errors array.
613
     * @return bool If saved or not.
614
     */
615
    public function setError(string $msg = '', string $key = ''): bool
616
    {
617
        if (!trim($msg)) {
618
            return false;
619
        }
620
        if ($key) {
621
            $this->errors[$key] = $msg;
622
        } else {
623
            $this->errors[] = $msg;
624
        }
625
626
        return true;
627
    }
628
629
    /**
630
     * Gets the accumulated error messages.
631
     * If a key is given, return that specific message. If that key doesn't exist, return false.
632
     *
633
     * @param string|null $key The key to the specific message to get.
634
     * @return array|string|false
635
     */
636
    public function getErrors($key = null)
637
    {
638
        if ($key) {
639
            if (isset($this->errors[$key])) {
640
                return $this->errors[$key];
641
            } else {
642
                return false;
643
            }
644
        }
645
646
        return $this->errors;
647
    }
648
}
649