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

JiraProject::buildSubmittedIssue()   F

Complexity

Conditions 12
Paths 385

Size

Total Lines 53
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 14.7316

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 12
eloc 30
c 2
b 0
f 0
nc 385
nop 2
dl 0
loc 53
ccs 22
cts 30
cp 0.7332
crap 14.7316
rs 3.8208

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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