Passed
Pull Request — master (#38)
by Brian
03:55 queued 39s
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
        $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